暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

接上期,MySQL行锁的加锁结论如何用实验来证明?长文适合收藏

运维路书 2024-11-18
77


上一期,讲了MySQL行锁的加锁结论和原理

运维还是研发的锅?MySQL的锁策略如何引发线上争端?

下面用具体的实验来证明一下结论,实验过程较长,适合收藏起来。

建议自己按照步骤再复现一遍,一切就都明白了。


核心要点



实验证明:

1. 读提交

  • 有索引

  • 无索引

2. 可重复读

  • 唯一索引

  • 非唯一索引

  • 无索引



读提交

RC的隔离级别下,只有记录锁这一种类型。

有索引和无索引的加锁方式有一定的差别

有索引



建一个实验表,主键为id, 二级索引age;


1

发起事务A


查询条件二级索引列age=22


查看一下 data_locks 






事务A 首先对二级索引列age=22 加了一把记录锁,同时对于该行记录的主键索引id=10也加了一把记录锁。意味着,其他事务无法更新或删除age=22和id=10的记录。


2

发起事务B



事务B 被阻塞,由于age=22的这行数据被事务A加了记录锁。



3

发起事务C



事务A只锁定了二级索引age=22的这一行记录,不存在间隙锁,不影响其他行数据的加锁。

结论:

        在RC隔离级别下,当一个事务对索引字段加锁后,不管是主键索引还是非唯一索引,只会锁住符合查询条件的记录,其他数据不会受影响。


无索引


无索引分为两种:

  • select ... for update

  • update

实验表t只有一个主键




select ... for update

1

发起事务A



查看锁的情况:id=2的数据行被锁住了,使用的X型记录锁



2

发起事务B




事务B先扫描了name='a'的记录,

将该行记录的主键索引 id=1加了一把记录锁。

接下来继续向后扫描,扫描到下一行对主键索引 id=2 加锁时,

发现事务A目前持有id=2的记录锁。

因此,发生事务B阻塞。


3

发起事务C 





事务C依然从第一行数据开始扫描,

在对id=1的主键索引加记录锁时,

发现事务B目前持有id=1的记录锁,

因此,发生阻塞。


结论:

        RC隔离级别下,select ...for update 在没有索引的条件下,扫描全表顺序加锁,再判断记录是否符合要求,符合就保留锁,不符合就释放锁。




update

1

发起事务A



2

发起事务B



事务B 成功执行


结论:

RC隔离级别下,

update与select ... for update的加锁方式不同,

update是先搜索出符合条件的数据,再加锁更新


可重复读


RR隔离级别下,行锁加锁规则比较复杂,不同场景下加锁的形式有所不同。

加锁的对象是索引,加锁的基本单位是 next-key lock。

next-key lock  【前开后闭区间】

间隙锁             【前开后开区间】

next-key lock 在一些场景下会退化成记录锁或间隙锁。

具体是哪些场景?

使用记录锁或者间隙锁足够避免幻读, 

next-key lock 就会退化成记录锁或间隙锁




唯一索引




等值查询





记录存在


1

发起事务A




显示这里加了记录锁


LOCK_MODE 三种类型

  • X, next-key 锁;

  • (X, REC_NOT_GAP),记录锁;

  • (X, GAP),间隙锁;


2

发起事务B






为什么唯一索引等值查询并且查询记录存在的场景下,索引的 next-key lock 会退化成记录锁?


原因就是仅靠记录锁就能避免幻读

要避免幻读就是避免结果集被其他事务修改或删除,

或者有其他事务插入了影响结果集的新记录。

  • 主键具有唯一性,所以其他事务插入 id = 1 的时候,会因为主键冲突,导致无法插入 id = 1 的新记录

  • 由于对 id = 1 加了记录锁,其他事务无法修改或删除该记录

这样事务 A 在多次查询  id = 1 的记录的时候,仅靠记录锁就不会出现前后两次查询的结果集不同,也就避免了幻读的问题。





记录不存在

1

发起事务A




事务 A 在 id = 5 的主键索引上加的是间隙锁,锁住的范围是 (1, 5)。


2

发起事务B




插入意向锁,在等待id为(1,5)的间隙锁释放。因此id=3的数据行无法插入显示阻塞


为什么唯一索引等值查询并且查询记录【不存在】的场景下,next-key lock 会退化成间隙锁?


  • 查询 id = 2 的记录,只要保证前后两次查询id=2的结果集相同,就能避免幻读的问题了。所以,即使 id =5 被删除,也不会有什么影响。那就没必要加 next-key lock,间隙锁足够用。避免其他事务插入 id = 2 的新记录就行了。

  • 为什么不可以针对不存在的记录加记录锁锁是加在索引上的,而这个场景下查询的记录是不存在的,自然就没办法锁住这条不存在的记录。


结论:

  • 当查询的记录是「存在」的,在索引树上定位到这一条记录后,将该记录的索引上的 next-key lock退化成「记录锁」

  • 当查询的记录是「不存在」的,在索引树找到第一条大于该查询记录的记录后,将该记录的索引上的 next-key lock退化成「间隙锁」



范围查询


1

大于等于



查看下 data_locks中的加锁情况


事务在主键索引上加了三个X型锁 

id=15的记录锁 

临键锁(15,20] 

临键锁(20,+]


事务加锁变化过程:


  1. 最开始要找的第一行是 id = 15,由于查询该记录是一个等值查询(等于 15),所以该主键索引的 next-key 锁会退化成记录锁,也就是仅锁住 id = 15 这一行记录。

  2. 由于是范围查找,就会继续往后找存在的记录,扫描到的第二行是 id = 20,于是对该主键索引加的是范围为  (15, 20] 的 next-key 锁;

  3. 接着扫描到第三行的时候,扫描到了特殊记录( supremum pseudo-record),于是对该主键索引加的是范围为  (20, +∞] 的 next-key 锁。

  4. 停止扫描。


加锁的效果:

  1. 其他事务无法更新或者删除 id = 15 的记录;

  2. 其他事务即无法更新或者删除 id = 20 的记录,同时无法插入 id 值为 16、17、18、19 的新记录;

  3. 其他事务无法插入 id 值大于 20 的新记录;

结论:

        「大于等于」的范围查询,如果等值查询的记录存在,那么该记录的索引上的 next-key 锁会退化成记录锁其他扫描到的记录,在索引上加 next-key 锁


2

小于或小于等于(数据不存在)


id=6的数据不存在

查看下 data_locks中的加锁情况:

事务在主键索引上加了三个X型锁

临键锁 (-∞, 1]

临键锁 (1, 5] 

间隙锁 (5, 10) 


事务加锁变化过程:

  1. 从第一行数据id=1开始查找,对该主键索引加(-∞,1]的next-key 锁

  2. 由于是范围查询,会继续向后查找,找到id=5的这条记录,于是对该主键索引加(1,5]的next-key锁

  3. 由于id=5的记录满足id<6的查询条件,所以没达到终止扫描的条件,继续向后扫描

  4. 扫描到第三行id=10 的数据,不满足id<6的条件,因此这行数据的锁会退化成间隙锁,于是对该主键索引加(5,10)的间隙锁

  5. 由于id=10 的记录不满足id<6的条件,停止扫描


加锁的效果:

  1. 其他事务即无法更新或者删除 id = 1 的记录,同时也无法插入 id 小于 1 的新记录;

  2. 其他事务无法更新或者删除 id = 5 的记录,同时也无法插入 id 值为 2、3、4 的新记录;

  3. 其他事务无法插入 id 值为 6、7、8、9 的新记录;

如果范围查询的条件改成 <= 6 ,由于id=6的数据行不存在,因此加锁范围还是和查询条件为<6的加锁范围一致。


结论:

        「小于或者小于等于」的唯一索引范围查询,如果条件值的记录不在表中,那么不管是「小于」还是「小于等于」的范围查询,扫描到终止范围查询的记录时,该记录中索引的 next-key 锁会退化成间隙锁,其他扫描到的记录,在索引上加 next-key 锁


非唯一索引




等值查询


1

记录存在




事务加了三个X型锁

主键:

记录锁 id=10

二级索引

临键锁 (21,22]

间隙锁 (22,30)




二级索引树如何存放记录?

二级索引树是按照二级索引值(age)的顺序存放的,在相同的二级索引值下,再按照主键顺序存放。



事务加锁变化过程:

  1. 由于二级索引是非唯一索引,可能存在多个age 为22的值,因此会先在索引字段age上加一把  (21,22]的next-key lock。


  2. 继续先后扫描,扫描到数据age=30, 这行数据为第一个不满足age=22的数据,将锁退化成(22,30)的间隙锁,同时停止扫描。


加锁的效果:

  1. 其他事务无法更改或删除age=22的记录;

  2. 其他事务将无法更改或删除id=10的这一行记录;

  3. 其他事务将无法插入 age值为 23,24,25 . . . 29的记录;


结论:

        非唯一索引等值查询,当查询记录【存在】时,由于不是唯一索引,索引字段可能存在相同值。因此,非唯一索引的等值查询是一个扫描过程,直到扫描到第一个不满足查询条件的记录才停止扫描。扫描过程中对于扫描到的二级索引记录加next-key lock, 而对于第一个不满足条件的二级索引记录加锁将退化成间隙锁,同时会在符合查询条件的记录的主键索引上加记录锁。


2

记录不存在


事务加了一个X型锁

间隙锁 (22,30)


事务加锁变化过程:

  1. 由于age=24的记录不存在,因此向后扫描到第一个不满足条件的记录后,停止扫描。该二级索引的next-key 锁会退化成间隙锁(22,30)

加锁的效果:

  1. 其他事务将无法插入 age值为 23,24,25 . . . 29的记录;

  2. 其他事务插入age=22;

  • 如果id<10,在二级索引树上定位的插入位置的下一条数据为id=10,age=22 ,这条记录上没有间隙锁,因此可以插入成功。

  • 如果id>10,在二级索引树上定位的插入位置的下一条数据为id=20,age=30 ,这条记录上有(22,30)间隙锁,插入阻塞。

     
3.其他事务插入age=30;


  • 如果id<20,在二级索引树上定位的插入位置的下一条数据为id=20,age=30 ,这条记录上有(22,30)间隙锁,插入阻塞。

  • 如果id>20,在二级索引树上定位的插入位置的下一条数据不存在,因此可以插入成功。


结论:

        非唯一索引等值查询,当查询记录【不存在】时,扫描到第一个不满足查询条件的记录时,该记录的索引会退化成间隙锁。同时由于查询记录不存在,因此不会在主键索引上加锁。





范围查询

事务加了五个X型锁

临键锁 (21,22]

临键锁 (22,30]

临键锁 (30,+∞]

记录锁 id=10

记录锁 id=20


事务加锁变化过程:

  1. 先找到age=22的记录,虽然这里包含等值查询,但是由于不是主键索引,因此不会退化成记录锁。直接加了一把(21,22]的next-key lock。

  2.  age=22的这条记录的主键索引id=10加一把记录锁;

  3. 向后扫描到age=30的记录,满足查询条件,因此会对该二级索引加一把(22,30]的next-key lock。

  4. age=30的这条记录的主键索引id=20加一把记录锁。

  5. age=30是最后一条记录,存储引擎会用一个特殊记录来标识最后一条记录,同时加一把(30,+∞]的 next-key lock

加锁的效果:

  1. 其他事务无法删除或更新 age=22的这行记录。对于是否能插入age=21 和age=22的新记录,需要根据id值来判断。

  2. 其他事务无法删除或更新 id=10 的这一行记录;

  3. 其他事务无法删除或更新 age=30的这行记录,其他事务无法插入age值范围为 23,24,25... 29的新记录。同时,age=22和age=30的新记录也无法插入。

  4. 其他事务无法删除或更新 id=20 的这一行记录;

  5. 其他事务无法插入age值大于30的记录;


结论:

        非唯一索引的范围查询不管是大于、大于等于、小于、小于等于,加锁方式都相同,在二级索引上都是加next-key 锁,同时对扫描到的数据的主键索引加一把记录锁。


无索引

如果查询条件没有用到索引列或者索引失效,会导致全表扫描,在每条记录的主键索引上加一把next-key lock。这就相当于锁住了全表,其他事务对该表的增、删、改 都会被阻塞。


总结

  • 读提交

    • 有索引

      • RC的隔离级别的行级锁类型只有记录锁,因此在有索引的情况下,只对符合查询条件的记录加记录锁。

    • 无索引

      • select ... for update,先对全表数据顺序加锁,然后再判断是否符合查询要求,符合的锁就保留,不符合的就释放,相当于全表锁。

      • update,先查找出符合要求的语句再加锁更新

  • 可重复读

           可重复读的隔离级别下,行级锁的加锁基本单位是 next-key lock。在不同的索引类型以及是否使用索引的不同情况下,next-key lock会相应的退化成记录锁或间隙锁。

    • 唯一索引等值查询

      • 查询记录【存在】,会将索引上的next-key lock 退化成记录锁

      • 查询记录【不存在】,扫描到第一条大于该查询条件的记录后,将该记录的next-key lock 退化成 间隙锁

    • 唯一索引范围查找

      • 大于等于,记录【存在】,记录的主键索引会退化成记录锁

      • 小于或小于等于,记录【不存在】,终止扫描行的记录上的主键索引会退化成间隙锁

      • 小于,记录【存在】,该记录的主键索引上的锁会退化成【间隙锁】。

    • 非唯一索引等值查询

      • 记录【存在】,由于不是唯一索引,因此可能存在多个相同索引值的情况。非唯一索引等值查询是一个扫描过程,直到扫描到第一个不满足查询条件的记录才停止扫描。对于扫描到的二级索引记录加next-key lock, 对于扫描到的第一个不满足查询条件的记录,二级索引会退化成间隙锁。同时,对于符合查询条件的记录的主键索引加记录锁

      • 记录【不存在】,扫描到第一条不符合查询条件的记录后,停止扫描。同时,将扫描到的记录二级索引的next-key lock 退化成 间隙锁。因为不存在符合查询条件的记录,所以不会对主键加记录锁


    • 非唯一索引范围查找


      • 非唯一索引范围查找,所有扫描到的数据都会加 next_key lock ,不存在锁退化的情况。同时,所有符合查询条件的记录还会在主键索引上加【记录锁】
    • 无索引的查询


      • 如果查询条件没有用到索引列或者索引失效,会导致全表扫描,在每条记录的主键索引上加一把next-key lock。这就相当于锁住了全表,其他事务对该表的增、删、改 都会被阻塞。生产环境中要尽量避免这类操作,否则会带来锁表,影响生产业务的情况发生。


【以上仅为个人观点,如有不同意见,欢迎留言讨论!】

点击蓝字 关注我们

文章转载自运维路书,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论