图解gap lock
快照读的可重复读是依赖MVCC实现的,而一致性读(也称作当前读)的可重复读还需要依靠间隙锁gap lock。
环境准备
- 克隆项目
https://github.com/cafewang/playground - 确认可以从命令行访问docker
- 在mysql-test目录下的运行
sh ./start_server启动mysql数据库 - 运行PrimaryKeyQueryTest和UniqueKeyQueryTest中的测试,注意jdk版本最低为11
mysql版本是8.0.41,不同版本下的锁表现可能不一致。
初始时user命名空间下有表employee,有主键id和带唯一索引的字段no,name默认为空串。
初始数据如下
| id | no | name |
|---|---|---|
| 5 | 105 | |
| 10 | 110 | |
| 15 | 115 | |
| 20 | 120 |
主键等值查询
命中
查询id=10的记录,在另一个事务中更新id=10的记录,更新事务被阻塞。
在新事务中插入id=9和id=12都不受影响。
未命中
查询id=12,在新事务中插入id=11会阻塞,更新id=10和id=15不受影响,间隙锁范围为(10,15)。
可见未命中的主键查询,会在主键之间加上间隙锁,防止查询期间其他事务插入数据。
主键范围查询
单点
查询范围为[10,11),即id >= 10 and id < 11。在新事务中
- id=9可以插入
- id=10无法更新
- id=12无法插入
- id=15可以更新
可以看到id < 11给(10, 15)加上了间隙锁,但是没给15加上行锁。
可以推断出,间隙锁会加到不满足查询条件的下一个主键,对于id < 11就是15,但是不会在下一个主键上加锁。
跨主键
查询id范围为[4, 12),在新事务中
- id=1不可插入
- id=5不可更新
- id=6不可插入
- id=10不可更新
- id=12不可插入
- id=15可以更新
实际上,此时的锁有
- 间隙锁:(-∞, 5), (5, 10), (10, 15)
- 行锁:5, 10
上限
查询id范围[16, 21),在新事务中
- id=15可更新
- id=16不可插入
- id=20不可更新
- id=24不可插入
此时间隙锁有(15, 20),(20, +∞),行锁有20。
总结一下,一致性读在主键上范围查询,范围两端落到存在的主键之间都会加上间隙锁,包括不等于的情况,
比如id < 11,会在(10, 15)上加间隙锁。
唯一索引等值查询
带唯一索引的字段和主键的加锁逻辑有所不同。
命中
查询no=110,会给唯一索引=110加行锁,以及对应主键索引10加行锁,不会影响其他记录的插入和更新。
未命中
查询no=112,在新事务中
- 可以通过no=110更新name字段
update employee set name = ? where no=110 - 不能插入no=114的记录
- 可以通过no=115更新
可以看出,在等值查询上,唯一索引和主键索引并没有什么区别。
唯一索引范围查询
单点
查询no范围为[110, 111)的记录,在新事务中
- 可以通过no=105更新
- 不能插入no=109
- 不能通过no=110更新
- 不能插入no=112
- 不能通过no=115更新
- 可以插入no=116
可以看出,和主键不同,唯一索引的一致性读会在查询范围两端都加上next-key锁,即间隙锁右端加上行锁。
no >= 110加上(105, 110]的锁,no < 111加上了(110, 115]的锁,
所以整体构成了(105, 115]的左开右闭区间。
跨端点
以no范围[105, 112)查询,在新事务中
- 不能插入no=101
- 不能插入no=108
- 不能插入no=114
- 不能通过no=115更新
- 可以插入no=116
next-key锁有(-∞, 105]、(105, 110]、(110, 115]。
上限
以no范围[115, 121)查询,在新事务中
- 可以通过no=110更新
- 不能插入no=114
- 不能插入no=117
- 不能插入no=130
next-key锁有(110, 115]、(115, 120]、(120, +∞)。 总结一下,唯一索引的加锁方式和主键并不一样:
- 使用next-key锁,会锁住查询范围外的下一个记录,如
no < 112,会锁住no=115的记录 - 只要是范围中的记录都会加上next-key锁,如
no >= 115,会加上(110, 115]的锁