MySQL 行级锁机制详解
InnoDB 引擎支持行级锁,而 MyISAM 引擎并不支持。顾名思义,行锁就是针对数据表中行记录的锁。比如事务 A 更新了一行,此时事务 B 也要更新同一行,则必须等事务 A 的操作完成后才能进行。
普通的 SELECT 语句属于快照读,不会对记录加锁。如果需要在查询时对记录加行锁,可以使用以下两种方式,这种查询称为锁定读:
-- 对读取的记录加共享锁
SELECT ... LOCK IN SHARE MODE;
-- 对读取的记录加独占锁
SELECT ... FOR UPDATE;
行锁类型
Record Lock(记录锁)
Record Lock 锁住的是具体的一条记录。它分为 S 锁(共享锁)和 X 锁(排他锁):
- 当一个事务对一条记录加了 S 型记录锁后,其他事务可以继续对该记录加 S 型记录锁,但不可加 X 型记录锁。
- 当一个事务对一条记录加了 X 型记录锁后,其他事务既不可以加 S 型也不可以加 X 型记录锁。
例如,执行 SELECT * FROM t_test WHERE id = 1 FOR UPDATE; 会对主键 id 为 1 的记录加上 X 型记录锁,其他事务无法修改该行。当事务提交或回滚后,锁会被释放。
Gap Lock(间隙锁)
Gap Lock 存在于可重复读隔离级别和串行化隔离级别,目的是解决幻读问题。假设表中有一个范围 (3, 5) 的间隙锁,其他事务就无法插入 id = 4 的记录。
Next-Key Lock(临键锁)
Next-Key Lock 是 Record Lock + Gap Lock 的组合,锁定一个范围并锁定记录本身。例如,范围 (3, 5] 的 Next-Key Lock 意味着其他事务不能插入 id = 4,也不能修改 id = 5 的记录。
如果一个事务获取了 X 型的 Next-Key Lock,另一个事务在获取相同范围的 X 型 Next-Key Lock 时会被阻塞。
SELECT ... FOR UPDATE 的作用
在 MySQL 默认的可重复读隔离级别下,普通 SELECT 是快照读,基于 MVCC 读取历史快照,不加锁。这虽然保证了高并发性能,但在特定场景下会导致问题,比如库存超卖。
场景示例:
- 事务 A 查询库存:
SELECT stock FROM products WHERE id = 1;(返回 1) - 事务 B 也查询库存:
SELECT stock FROM products WHERE id = 1;(返回 1) - 事务 A 下单扣减并提交,库存变为 0。
- 事务 B 也扣减并提交,库存变为 -1。
使用 FOR UPDATE 解决:
- 事务 A 执行
SELECT ... FOR UPDATE,对 id=1 的记录加排他锁。 - 事务 B 尝试执行同样的语句,会被阻塞直到事务 A 释放锁。
- 事务 A 提交后,事务 B 获得锁,读到更新后的库存 0,从而阻止后续扣减。
MySQL 是如何加行级锁的?
加锁的对象是索引,基本单位是 Next-Key Lock。但在某些场景下,Next-Key Lock 会退化为记录锁或间隙锁。
实验表结构如下:
CREATE TABLE `user` (
`id` bigint AUTO_INCREMENT,
`name` () utf8mb4_unicode_ci ,
`age` ,
(`id`),
KEY `index_age` (`age`) BTREE
) ENGINEInnoDB CHARSETutf8mb4 utf8mb4_unicode_ci;

