🚀 AI篇持续更新中!(长期更新)
AI炼丹日志-31- 千呼万唤始出来 GPT-5 发布!“快的模型 + 深度思考模型 + 实时路由”,持续打造实用AI工具指南!📐🤖
💻 Java篇正式开启!(300篇)
目前2025年08月18日更新到:
Java-100 深入浅出 MySQL事务隔离级别:读未提交、已提交、可重复读与串行化
MyBatis 已完结,Spring 已完结,Nginx已完结,Tomcat已完结,分布式服务正在更新!深入浅出助你打牢基础!
📊 大数据板块已完成多项干货更新(300篇):
包括 Hadoop、Hive、Kafka、Flink、ClickHouse、Elasticsearch 等二十余项核心组件,覆盖离线+实时数仓全栈!
大数据-278 Spark MLib - 基础介绍 机器学习算法 梯度提升树 GBDT案例 详解
锁机制
悲观锁
悲观锁(Pessimistic Locking)是一种并发控制机制,它基于"先锁定,后修改"的保守策略。在数据处理过程中,悲观锁会假设并发操作很可能导致冲突,因此会先将数据置于锁定状态,确保在当前事务完成前其他事务无法修改该数据。这种锁机制通常通过数据库内建的锁功能来实现。
从实现方式来看,悲观锁主要分为以下几种类型:
- 行级锁(Row Lock):锁定单行数据,如MySQL的SELECT…FOR UPDATE语句
- 表级锁(Table Lock):锁定整个数据表
- 读锁(Shared Lock):允许多个事务同时读取数据但禁止修改
- 写锁(Exclusive Lock):只允许一个事务读写数据,其他事务完全禁止访问
典型的应用场景包括:
- 银行账户转账操作
- 库存管理系统中的库存扣减
- 机票预订系统中的座位锁定
以MySQL为例,悲观锁的实现方式如下:
BEGIN;
SELECT * FROM accounts WHERE id=1 FOR UPDATE; -- 获取排他锁
UPDATE accounts SET balance=balance-100 WHERE id=1;
COMMIT;
悲观锁的优缺点:
优点:能有效避免数据冲突,保证数据一致性
缺点:会降低系统并发性能,可能导致死锁
在分布式系统中,悲观锁的实现会更加复杂,通常需要借助Redis、Zookeeper等中间件来实现跨服务的锁管理。
表级锁
表级锁每次操作都会锁住整张表,并发度最低。
手动增加表锁:
lock table 表 read|write, 表2 read|write;
查看表上加过的锁:
show open tables;
执行结果如下:
删除表锁:
unlock tables;
MySQL表级锁详解
表级读锁(Table Read Lock)
当对表添加读锁时:
-
当前连接:
- 可以执行SELECT查询操作
- 不能执行INSERT、UPDATE、DELETE等写操作,否则会立即报错
- 示例:
LOCK TABLES user READ;
后尝试执行UPDATE user SET name='Tom' WHERE id=1;
会抛出错误
-
其他连接:
- 可以并发执行SELECT查询操作
- 尝试执行写操作(INSERT/UPDATE/DELETE)会被阻塞,直到锁被释放
- 应用场景:在数据备份时,需要确保备份期间数据不被修改
表级写锁(Table Write Lock)
当对表添加写锁时:
-
当前连接:
- 可以执行所有操作(SELECT/INSERT/UPDATE/DELETE)
- 示例:
LOCK TABLES user WRITE;
后可以自由读写该表
-
其他连接:
- 所有操作(包括SELECT查询)都会被阻塞
- 阻塞会持续到锁持有者释放锁(执行UNLOCK TABLES)
- 典型应用:批量数据导入时要避免其他会话干扰
锁特性对比
特性 | 读锁 | 写锁 |
---|---|---|
当前连接读操作 | 允许 | 允许 |
当前连接写操作 | 禁止 | 允许 |
其他连接读操作 | 允许 | 阻塞 |
其他连接写操作 | 阻塞 | 阻塞 |
锁冲突 | 允许多个读锁共存 | 与其他所有锁互斥 |
注意:表级锁是MySQL最基本的锁机制,在MyISAM存储引擎中为默认锁类型。在高并发场景下,使用表级锁可能导致严重的性能问题。
共享锁(行级锁-读锁)
共享锁又称为读锁,简称S锁,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。使用共享锁的方法是:SELECT lock in share mode,只适用查询语句。
排他锁(行级锁-写锁)
排他锁(Exclusive Lock),又称写锁,简称X锁,是一种基本的数据库锁机制。这种锁具有排他性,即当一个事务获取了某个数据行的排他锁后,其他事务既不能对该行记录进行任何修改操作,也不能获取该行的任何类型的锁(包括共享锁和排他锁)。
排他锁的特性
- 排他性:排他锁与其他任何锁(包括共享锁和其他排他锁)互斥,不能共存
- 独占访问:持有排他锁的事务可以读取和修改数据
- 阻塞其他操作:其他事务不能修改数据,也不能通过
for update
子句获取新的记录锁
使用方式
在SQL语句中,可以通过在查询语句末尾添加FOR UPDATE
子句显式地获取排他锁:
SELECT * FROM employees WHERE id = 1 FOR UPDATE;
InnoDB引擎的默认行为
- 在
UPDATE
和DELETE
语句中,InnoDB引擎会自动为操作的行加上排他锁,相当于隐式地添加了FOR UPDATE
- 这些操作不需要显式指定
FOR UPDATE
子句
实现机制
排他锁的行级锁实现依赖于表上的索引:
- 使用索引的情况:当操作使用索引进行查询时,InnoDB只会锁定符合条件的行记录
- 不使用索引的情况:如果查询没有使用到索引(如全表扫描或对非索引列进行查询),InnoDB会退化为锁定整个表的所有记录,这可能导致严重的性能问题和并发性降低
实际应用场景
-
库存扣减:在电商系统中,防止超卖
BEGIN; SELECT quantity FROM products WHERE product_id = 1001 FOR UPDATE; UPDATE products SET quantity = quantity - 1 WHERE product_id = 1001; COMMIT;
-
账户转账:确保资金操作的一致性
BEGIN; SELECT balance FROM accounts WHERE account_id = 12345 FOR UPDATE; -- 执行转账操作 COMMIT;
注意事项
- 排他锁会降低系统的并发性能,应尽量缩短持有锁的时间
- 避免在事务中执行耗时的操作(如网络请求)时持有锁
- 确保查询使用适当的索引,防止意外锁表
- 在事务结束时(提交或回滚)会自动释放所有排他锁
乐观锁
乐观锁是一种并发控制机制,与悲观锁形成鲜明对比。它不是数据库系统内置的功能,而是需要开发者自行设计实现的。这种机制得名于其乐观的假设:系统认为当前事务不会与其他事务发生冲突,因此在数据操作阶段不会采取任何锁机制。
具体实现原理:
- 数据访问阶段:事务在读取数据时不加任何锁,可以自由读取数据
- 数据修改阶段:事务在修改数据时也不加锁
- 提交验证阶段:在事务准备提交时,系统会检查数据是否被其他事务修改过
实现关键点:
- 版本号机制:常用的实现方式是为数据表增加version字段
- 时间戳机制:使用最后修改时间戳进行冲突检测
- 条件检查:通过比对特定字段值判断数据是否被修改
应用场景对比:
-
乐观锁适用场景:
- 读多写少的应用(如资讯类网站)
- 并发冲突概率较低的系统
- 需要高吞吐量的业务场景
- 示例:电商系统中的库存管理(多个用户同时浏览商品但购买操作较少)
-
悲观锁适用场景:
- 写操作频繁的应用
- 数据一致性要求极高的系统
- 并发冲突概率高的场景
- 示例:银行系统的账户余额修改
性能考量:
- 乐观锁优势:
- 减少锁开销
- 提高系统吞吐量
- 避免死锁问题
- 悲观锁优势:
- 保证强一致性
- 实现简单直接
- 适合长事务处理
实际开发建议:
- 根据业务特点选择:
- 高并发读场景优先考虑乐观锁
- 关键数据强一致场景考虑悲观锁
- 可以结合使用:
- 某些系统可以针对不同业务模块采用不同策略
- 例如电商系统可以用乐观锁管理商品浏览,用悲观锁处理订单支付
实现原理
乐观锁的实现原理:
-
版本字段机制
乐观锁通常通过在数据表中添加一个version版本号字段来实现。这个字段可以是整数类型(如INT)或时间戳类型(TIMESTAMP)。 -
工作流程
- 读取阶段:事务A读取数据时,同时获取该数据的当前版本号(如version=1)
- 修改阶段:事务A准备更新数据时,会先检查当前版本号是否仍为1
- 提交阶段:如果版本号匹配,则执行更新并将版本号+1(version=2);如果不匹配,则说明数据已被其他事务修改,当前操作失败
-
具体SQL示例
-- 读取阶段 SELECT id, name, version FROM products WHERE id = 1; -- 更新阶段(原子操作) UPDATE products SET name = '新商品名称', version = version + 1 WHERE id = 1 AND version = 1;
-
适用场景
- 读多写少的应用场景
- 并发冲突较少的情况
- 需要减少锁竞争的系统
- 分布式系统环境
-
变体实现方式
- 时间戳方式:使用last_updated_time字段替代version字段
- 条件更新:通过其他业务字段作为条件校验(如库存数量)
-
注意事项
- 需要处理更新失败的情况(通常需要重试机制)
- 不适合高并发写场景(会导致大量失败重试)
- 不能解决ABA问题(需要额外措施如递增版本号)
此外也可以使用时间戳:
与使用 version 版本字段类似,同样需要给数据表增加一个字段,字段类型使用 timestamp时间戳。也是在更新提交的时候检查当前数据库中的数据的时间戳和自己更新前获取到的时间戳是否一致,不一致就是版本冲突,取消操作。
实现案例
比如第一步查询商品的信息:
SELECT number, version for products where id=1;
根据商品信息生成订单:
INSERT INTO orders ...
INSERT INTO items ...
修改商品的库存:
UPDATE products SET number=number-1, version=version+1 WHERE id=1 nad version=#{version}
PS:当然除了我们手动实现乐观锁以外,许多框架也实现了乐观锁,比如MyBatis我们可以使用 OptimisticLocker 插件来扩展。