一次并发插入死锁带来的“教训”,我才清楚这些MySQL锁知识( 二 )

一次并发插入死锁带来的“教训”,我才清楚这些MySQL锁知识
文章插图
 
上图基本流程如下:

一次并发插入死锁带来的“教训”,我才清楚这些MySQL锁知识

文章插图
 
从上面的实测也可以看出 , 普通索引下添加x锁 , 居然会加一个gap锁 , 而且这个gap区间是前一个记录(并包含它) , 到下一个记录
如 uid = 20 ,  前后两个记录为(1, 10), (10, 30)
  • gap lock: 范围为 [10, 30)
  • 因此无法插入uid=[10,30)
  • 注意 , uid=10上有gap锁只是不能插入记录 , 但是加X锁是没有问题的(有兴趣的可以测试一下)
3.3 精确查询未匹配
当我们锁的记录不存在时 , 锁情况如下:
一次并发插入死锁带来的“教训”,我才清楚这些MySQL锁知识

文章插图
 
【一次并发插入死锁带来的“教训”,我才清楚这些MySQL锁知识】实测case如下(TN省略 , 锁全表的没啥测试必要性)
基本流程就不画图了 , 上面图中已经有文字描述了

一次并发插入死锁带来的“教训”,我才清楚这些MySQL锁知识

文章插图
 
从上面的测试也可以看出 , uid=30没有被锁住 , 这里只在uid=(20, 30)这一区间添加了gap锁
唯一索引与普通索引表现一致 , 会阻塞insert的插入意向锁(后面说这个东西)
3.4 范围查询
当我们锁一段区间时 , 锁的情况如下:
一次并发插入死锁带来的“教训”,我才清楚这些MySQL锁知识

文章插图
 

一次并发插入死锁带来的“教训”,我才清楚这些MySQL锁知识

文章插图
 
简单来说 , 范围查询时 , 添加next key lock , 根据我们的查询条件 , 找到最左边和最右边的记录区间
如 uid > 15 and uid < 25 , 找到的记录是(1, 10), (10, 30)
  • gap锁为(10, 30)
  • next key lock会为右边添加行锁 , 即uid=30加X锁
  • 因此针对uid=30记录加锁会被阻塞(但是针对uid=28,29加x锁则不会被阻塞 , 插入会 , 有兴趣的小伙伴可以实测一下)
说明:范围加x锁时 , 可能锁住不在这个区间的记录 , 一不小心可能导致死锁哦
3.5 小结
在RR隔离级别中 , 我们一般认为可以产生锁的语句为:
  • SELECT ... FOR UPDATE: X锁
  • SELECT ... LOCK IN SHARE MODE: S锁
  • update/delete: X锁

一次并发插入死锁带来的“教训”,我才清楚这些MySQL锁知识

文章插图
 
| 普通索引 | 精确匹配 , 且命中 | 行锁 + gap lock (上一个记录和下个记录区间 , 左闭右开 , 左边记录非行锁) | 普通索引 | 精确匹配 , 未命中 | gap lock | | 普通索引 | 范围查询 | next key lock |
4. 锁冲突
上面介绍了不同场景下会产生什么样的锁 , 但是看完之后会有一个疑问 , 针对行锁其他会话竞争的时候 , 可以按照X/S锁的规则来 , 但是这个GAP LOCK貌似只针对insert有效 , insert除了加X锁之外是不是还有其他的特殊逻辑?
4.1 插入意向锁
插入意向锁其实是一种特殊的 gap lock , 但是它不会阻塞其他锁 。假设存在值为 4 和 7 的索引记录 , 尝试插入值 5 和 6 的两个事务在获取插入行上的排它锁之前使用插入意向锁锁定间隙 , 即在(4 , 7)上加 gap lock , 但是这两个事务不会互相冲突等待;但是如果这个区间存在gap lock , 则会被阻塞;如果多个事务插入相同数据导致唯一冲突 , 则在重复的索引记录上加读锁
简单来说 , 它的属性为:
  • 它不会阻塞其他任何锁;
  • 它本身仅会被 gap lock 阻塞
其次一个重要知识点: