InnoDB Next-key 锁详解
准备
** MySQL 8.0.25 **
背景
InnoDB 的事务锁类型分为表锁和行锁 (record lock), 具体的锁组织管理方式可见数据库内核月报 Innodb 锁子系统浅析 和 InnoDB 事务 sharded 锁系统优化 . 针对行锁, InnoDB 为了不同的使用场景划分了不同的锁类型, 包括 LOCK_GAP, LOCK_REC_NOT_GAP, LOCK_INSERT_INTENTION 等. InnoDB 使用一个 ulint 类型来定义锁:
| record lock type | lock_type | lock_mode(低四位) |
低 4 位表示 lock_mode, 即
|
lock_type 占用 5-8 bit 位,目前只用了 5 和 6 位,大小为 16 和 32 ,表示 LOCK_TABLE 和 LOCK_REC, 第 9 为标志为 LOCK_WAIT, 表示该 lock 是否获取成功, 剩下的高位表示 record lock type:
|
锁定义
LOCK_REC_NOT_GAP
record 锁, 只锁住 record 本身.
LOCK_GAP
表示只锁住一段范围,不锁记录本身,通常表示两个索引记录之间,或者索引上的第一条记录之前,或者最后一条记录之后的锁.
LOCK_ORDINARY(Next-Key Lock)
NEXT-KEY 锁,即包含 record 本身及 record 之前的GAP。当前 MySQL 默认情况下使用 RR 的隔离级别,而 NEXT-KEY LOCK 正是为了解决 RR 隔离级别下的幻读问题。所谓幻读就是一个事务内执行相同的查询,会看到不同的行记录, 详见InnoDB 事务分析-MVCC.
插入意向锁
隐式锁
InnoDB 通常对插入操作无需加锁,但是存在一个 trx1 插入一条 record 后但尚未 commit, 而另外一个 trx2 试图修改这条 record, 为了保证事务隔离性, InnoDB 通过一种"隐式锁"的方式来解决冲突. 聚集索引记录中存储了 trx_id,而二级索引页 Page 中记录了当前最大的 trx_id, 当 trx2 试图修改这条 record 时,会去判断该 record 对应的 trx_id 是否属于一个活跃的事务即 trx1,之后会创建一个记录锁,然后将显式的记录锁置于等待队列中, 即 trx2 进入 wait 状态. 该设计的思路是基于大多数情况下新插入的记录不会立刻被别的线程并发修改,而创建锁的开销是比较昂贵的,涉及到全局资源的竞争. 通过隐式锁这个设计来避免锁资源申请的浪费.
Next-key 锁的加锁时机
Next-key 锁是 LOCK_REC_NOT_GAP 和 LOCK_GAP 的组合, 假如一张表存在id字段:
|
假定表child的id字段包括值 90, 102, 当用户执行上述语句时,InnoDB 会在 102 添加 Next-key 锁,防止其他的 session 在该 SELECT 执行过程中添加新的 record, 从而可能造成幻读.