MySQL是怎么加行级锁的?
MySQL是怎么加行级锁的?
假设有一张表 user,只有三个字段 id (主键索引) 、 age(非唯一索引)和name(无索引)。 表中现有数据:
id: 10, age: 10_v1, name: aaaid: 11, age: 10_v2, name: bbbid: 20, age: 20, name: cccid: 30, age: 30, name: ddd
1. 唯一索引等值查询
1.1 等值查询存在
1 | |
先给(-∞, 10]加上next-key锁,然后退化成id = 10处的记录锁
1.2 等值查询不存在
1 | |
先给(11, 20]加上next-key锁,然后退化成(11, 20)的间隙锁
2. 唯一索引范围查询
1 | |
id = 20的记录锁:从(11, 20]的next-key锁退化而来(20, 30)的间隙锁:从(20, 30]的next-key锁退化而来
1 | |
(20, 30]的next-key锁(11, 20]的next-key锁
3. 非唯一索引等值查询
3.1 等值查询存在
1 | |
(-∞, 10_v1]的next_key锁(10_v1, 10_v2]的next_key锁(10_v2, 20)的间隙锁id = 10和id = 11的记录锁
[!CAUTION]
由于这里是select *,所以会触发回表,去查找主键索引的B+树,也就会给
id = 10和id = 11这两条记录加上记录锁如果是select id且用的不是
for update而是lock in share mode,会出现索引覆盖,不会触发回表,也就不会在主键上加锁
3.2 等值查询不存在
1 | |
(10_v2, 20)的间隙锁
4. 非唯一索引范围查询
1 | |
(-∞, 10_v1]的next-key锁[!CAUTION]
这里貌似很反常识:为什么比10要小的区间也要锁起来呢?这是因为age不是唯一索引,如果不锁这个区间,那么就很有可能出现一条:id = 9(< 10), age = 10_v0的记录插进来,这时候再执行这条语句就会出现三条记录,导致幻读
(10_v1, 10_v2]的next-key锁(10_v2, 20]的next-key锁(20, 30]的next-key锁(30, +∞]的next-key锁id = 10, 11, 20, 30的记录锁(虽然发生了索引覆盖,但是是for update,系统会认为你接下来要更新数据,因此会顺便给主键上满足条件的行加锁)
1 | |
(-∞, 10_v1]的next-key锁(10_v1, 10_v2]的next-key锁(10_v2, 20]的next-key锁id = 10, 11的记录锁[!CAUTION]
注意,这里并没有退化成
(10_v2, 20)的间隙锁,而是依然是(10_v2, 20]的next-key锁;所以如果此时有另一个事务想要执行:insert into user values (16, 16, eee)也会被阻塞,这似乎也很反直觉……为什么不像等值查询那样退化?
你可能会问:“引擎明明已经判断出 20 >=15 了,为什么不把 20上的锁释放掉,只留间隙锁?”
这是因为 MySQL 在代码实现上,“等值查询”和“范围查询”走的是不同的加锁逻辑分支:
- 等值查询 (Equality Search):
- MySQL 专门做了一个优化:如果查找的是
age = 20,当扫描到age = 30时,发现值不匹配,判定这是边界,且搜索类型是等值,于是将 Next-Key Lock 专门退化为 Gap Lock。
- MySQL 专门做了一个优化:如果查找的是
- 范围查询 (Range Search):
- 对于
age >= 10 AND age < 15,MySQL 将其视为一个范围扫描。 - 在非唯一索引上,MySQL 的设计原则是“宁可多锁,不可漏锁”。当扫描到
age = 20时,虽然它不满足< 15,但它作为扫描停止的边界,InnoDB 并没有为“非唯一索引的范围查询”做类似于等值查询的那种“退化优化”。- 我的猜想:可能是因为在单边的范围查询如:上面的
age >= 10的情况下,锁全都是next-key锁,那么在这种双边的范围查询就不特殊处理了,和单边范围查询保持一致,也是全用next-key锁,处理起来更简便一些
- 我的猜想:可能是因为在单边的范围查询如:上面的
- 对于
- 等值查询 (Equality Search):
因此要记住:非唯一索引和主键索引的范围查询的加锁有所不同,不同之处在于非唯一索引范围查询,next-key lock 不会退化为间隙锁和记录锁。
5. 没有加索引的查询
如果select for update查询语句,没有使用索引列作为查询条件,或者查询语句没有走索引查询,导致扫描是全表扫描。那么,每一条记录的索引上都会加上next-key锁(主键索引,因为扫描的是主键的B+树),这样就相当于锁住全表。
同理delete, update语句查询条件不加索引也会导致锁全表的情况。