MySQL 中的事务隔离界别和各种锁
1. 行级锁/记录锁#
1.1. 排它锁 X 锁 - 解决脏写#
相同数据行上的 X 锁会阻塞其它加共享锁或排他锁的操作
SELECT * FROM my_table WHERE id = 1 FOR UPDATE;
在任何事务隔离级别下, 删改操作都需要先获得 x 锁, 目的是避免脏写, 也就是说即使没有显示使用
SELECT ... FOR UPDATE
, 进行删改操作时, 数据库都会自动尝试获得相应行的 x锁, 然后再进行写操作, 否则阻塞等待锁释放或当作死锁处理
1.2. 共享锁 S 锁 - 解决不可重复读#
允许多个事务同时读取同一数据, 但不允许修改, 也就是说与 X 锁互斥
SELECT * FROM my_table WHERE id = 1 LOCK IN SHARE MODE;
在 所有事务隔离级别 下, s锁都不会默认添加, 只有当显式使用
SELECT ... LOCK IN SHARE MODE
时才会加 s锁
拓展 在 InnoDB 可重复读 隔离级别下, 普通的
SELECT
语句不会默认加 s共享锁, 它使用的是一致性读, 即通过MVCC 机制读取数据的快照, 而不是加锁读取, 来避免不可重复读问题的, 当然你也可以显示加上 s共享锁, 使用锁来避免不可重复读问题,不可重复读: 发生在同一行数据被更新, 导致同一查询多次执行返回不同的具体数值
2. Next-Key Lock - 解决幻读#
注意 InnoDB 可重复读
或更高隔离级别才会有 Next-Key 锁, Next-Key 锁 = 记录锁 + 间隙锁,
Next-Key 锁的目的是解决幻读问题, 幻读是指事务读取一个范围的数据时, 另一个事务插入或删除行, 导致再次读取范围时结果不一致, 所以我们在进行范围查询时, 通过 SELECT ... FOR UPDATE
自动添加间隙锁, 这样就可以避免在间隙锁释放之前, 其他事务在查询范围内插入新纪录, 至于如何避免的, 我们下面会讲到,
2.1. 范围查询#
当执行涉及范围条件的查询且使用 SELECT ... FOR UPDATE
时, InnoDB 会使用 Next-Key Lock 锁定查询范围内的记录及其间隙
SELECT * FROM table_name WHERE id > 5 AND id < 10 FOR UPDATE;
注意上面的 SQL 语句格式
SELECT ... FOR UPDATE
, 若是普通查询SELECT ...
, 无论是否是范围查询 InnoDB 采用的都是 MVCC 多版本并发控制
2.2. 非唯一索引上的等值查询#
START TRANSACTION;
SELECT * FROM employees WHERE salary = 4000 FOR UPDATE;
如果 salary
列上存在索引, 并且是非唯一索引, InnoDB 会在查找到的记录上加锁, 同时加锁该记录与相邻记录之间的间隙, 即使这不是范围查询, 这保证了其他事务不能在锁定区间内插入新记录,从而避免幻读问题。因为若 salary 不唯一, 我们查到的结果可能是多个, 此时必须使用 Next-Key Lock 来防止其他事务插入相同的 salary 导致幻读,
如果 salary
唯一, 通常不会触发间隙锁, 只会加记录锁, 因为等值查询可以精确定位到一条记录, 不需要保护范围,
如果 salary
唯一, 但若是范围查询, 则需要防止其他事务在查询范围之间插入新记录, 因此仍会触发间隙锁,
如果 salary
列上不存在索引(无论是唯一索引还是非唯一索引),MySQL 的 InnoDB 在执行涉及 salary
的查询或修改操作时,会因为无法利用索引而退化为全表扫描, 在这种情况下,锁的机制和范围会发生显著变化,尤其是在默认隔离级别下,可能会导致更广泛的锁范围,甚至锁住整个表。
记录锁 Record Lock:锁定具体的索引记录, x 锁或 s 锁
间隙锁 Gap Lock:锁定索引记录之间的间隙, 防止其他事务在该范围内插入新记录, 间隙锁只在 可重复读 和 串行化 隔离级别下生效, 且只作用于 索引(包括主键索引和二级索引), 如果表没有索引(例如纯堆表), InnoDB 会退化为锁全表
3. 插入意向锁 Insert Intention Lock#
在 InnoDB 中,插入操作本身并不会直接获取行上的 X 锁, 因为插入的目标是新记录, 尚未存在于表中, 所以没有“行”可以加锁,
当执行 INSERT 操作时,InnoDB 不直接在数据行上加锁,而是先在目标 gap 上加上插入意向锁, 插入意向锁是 MySQL InnoDB 存储引擎中一种特殊的间隙锁变种, 属于意向锁的一种, 插入意向锁本身并不直接锁定具体的行, 而是表明一个事务“打算”在某个间隙中插入数据, 它会等待现有的间隙锁(如果有)释放, 从而避免冲突,
插入意向锁通常出现在使用 可重复读 隔离级别时, 因为在这个隔离级别下, MySQL 会使用间隙锁来防止幻读问题,
我们来看一下例子, 现在有两个事务同时尝试插入记录:
- 事务 A 想插入 id = 2
- 事务 B 想插入 id = 3
假设事务 C 先执行了一条查询 SELECT ... FOR UPDATE
, 锁定了间隙 (1, 4):
-
事务 A 请求插入 id = 2, 发现间隙 (1, 4) 被间隙锁占用, 插入意向锁被阻塞
-
事务 A 必须等待事务 C 提交或回滚, 释放间隙锁后才能继续插入
这就完美避免了幻读问题, 看看上面的内容, 前后呼应
4. 记录锁 多版本并发控制#
普通 SELECT
语句:
- 使用 MVCC 多版本并发控制
- 读取的是事务开始时的快照数据, 不会加
S
锁, 也不会阻塞其他事务的更新或插入
SELECT ... LOCK IN SHARE MODE
:
- 会显式对读取的行加
S
锁,其他事务仍可读取,但不能UPDATE
或DELETE
这些被锁的行 - 适用于确保数据在事务执行期间不会被修改,但仍允许读取
SELECT ... FOR UPDATE
:
- 会显式对读取的行加
X
锁,阻止其他事务对这些行进行UPDATE
或DELETE
,但仍允许读取(非锁定读)
总结
- 记录锁 Record Lock:锁定具体的索引记录, x 锁或 s 锁
- 间隙锁 Gap Loc:锁定索引记录之间的间隙,防止其他事务在该范围内插入新记录
- Next-Key 锁 = 记录锁 + 间隙锁
注意上面讨论的是在 InnoDB
可重复读
隔离级别下的情况, InnoDB 的默认隔离级别也是可重复读
, 更低级别比如已提交读
,已提交读
并不支持 MVCC 快照读 或者 Next-Key 锁, 它们都是可重复读
和序列化
隔离级别下的机制
5. 快照读 vs 当前读#
快照读: 普通的 SELECT, 不会加锁, 只依赖快照读 MVCC
当前读: SELECT ... FOR UPDATE
或 SELECT ... LOCK IN SHARE MODE
, 触发 x 锁 或 s 锁, 范围查询会触发 Next-Key Lock
- 无范围查询时, 当前读触发记录锁( x 锁 或 s 锁)
- 存在范围查询, 触发 Next-Key Lock, 也就是 记录锁 + 间隙锁
注释上面讨论的是 InnoDB “可重复读” 隔离级别下的情况