下面用具体的实验来证明一下结论,实验过程较长,适合收藏起来。
建议自己按照步骤再复现一遍,一切就都明白了。
核心要点

实验证明:
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,+
]

事务加锁变化过程:
最开始要找的第一行是 id = 15,由于查询该记录是一个等值查询(等于 15),所以该主键索引的 next-key 锁会退化成记录锁,也就是仅锁住 id = 15 这一行记录。
由于是范围查找,就会继续往后找存在的记录,扫描到的第二行是 id = 20,于是对该主键索引加的是范围为 (15, 20] 的 next-key 锁;
接着扫描到第三行的时候,扫描到了特殊记录( supremum pseudo-record),于是对该主键索引加的是范围为 (20, +∞] 的 next-key 锁。
停止扫描。
加锁的效果:
其他事务无法更新或者删除 id = 15 的记录;
其他事务即无法更新或者删除 id = 20 的记录,同时无法插入 id 值为 16、17、18、19 的新记录;
其他事务无法插入 id 值大于 20 的新记录;
结论:
「大于等于」的范围查询,如果等值查询的记录存在,那么该记录的索引上的 next-key 锁会退化成记录锁。其他扫描到的记录,在索引上加 next-key 锁。
2
小于或小于等于(数据不存在)

id=6的数据不存在
查看下 data_locks中的加锁情况:

事务在主键索引上加了三个X型锁
临键锁 (-∞, 1]
临键锁 (1, 5]
间隙锁 (5, 10)
事务加锁变化过程:
从第一行数据id=1开始查找,对该主键索引加(-∞,1]的next-key 锁
由于是范围查询,会继续向后查找,找到id=5的这条记录,于是对该主键索引加(1,5]的next-key锁
由于id=5的记录满足id<6的查询条件,所以没达到终止扫描的条件,继续向后扫描
扫描到第三行id=10 的数据,不满足id<6的条件,因此这行数据的锁会退化成间隙锁,于是对该主键索引加(5,10)的间隙锁
由于id=10 的记录不满足id<6的条件,停止扫描
加锁的效果:
其他事务即无法更新或者删除 id = 1 的记录,同时也无法插入 id 小于 1 的新记录;
其他事务无法更新或者删除 id = 5 的记录,同时也无法插入 id 值为 2、3、4 的新记录;
其他事务无法插入 id 值为 6、7、8、9 的新记录;
如果范围查询的条件改成 <= 6 ,由于id=6的数据行不存在,因此加锁范围还是和查询条件为<6的加锁范围一致。
结论:
「小于或者小于等于」的唯一索引范围查询,如果条件值的记录不在表中,那么不管是「小于」还是「小于等于」的范围查询,扫描到终止范围查询的记录时,该记录中索引的 next-key 锁会退化成间隙锁,其他扫描到的记录,在索引上加 next-key 锁。
非唯一索引
等值查询
1
记录存在


事务加了三个X型锁
主键:
记录锁 id=10
二级索引
临键锁 (21,22]
间隙锁 (22,30)


二级索引树如何存放记录?
二级索引树是按照二级索引值(age)的顺序存放的,在相同的二级索引值下,再按照主键顺序存放。
事务加锁变化过程:
由于二级索引是非唯一索引,可能存在多个age 为22的值,因此会先在索引字段age上加一把 (21,22]的next-key lock。
继续先后扫描,扫描到数据age=30, 这行数据为第一个不满足age=22的数据,将锁退化成(22,30)的间隙锁,同时停止扫描。
加锁的效果:
其他事务无法更改或删除age=22的记录;
其他事务将无法更改或删除id=10的这一行记录;
其他事务将无法插入 age值为 23,24,25 . . . 29的记录;
结论:
非唯一索引等值查询,当查询记录【存在】时,由于不是唯一索引,索引字段可能存在相同值。因此,非唯一索引的等值查询是一个扫描过程,直到扫描到第一个不满足查询条件的记录才停止扫描。扫描过程中对于扫描到的二级索引记录加next-key lock, 而对于第一个不满足条件的二级索引记录加锁将退化成间隙锁,同时会在符合查询条件的记录的主键索引上加记录锁。
2
记录不存在


事务加了一个X型锁
间隙锁 (22,30)

事务加锁变化过程:
由于age=24的记录不存在,因此向后扫描到第一个不满足条件的记录后,停止扫描。该二级索引的next-key 锁会退化成间隙锁(22,30)
加锁的效果:
其他事务将无法插入 age值为 23,24,25 . . . 29的记录;
其他事务插入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

事务加锁变化过程:
先找到age=22的记录,虽然这里包含等值查询,但是由于不是主键索引,因此不会退化成记录锁。直接加了一把(21,22]的next-key lock。
age=22的这条记录的主键索引id=10加一把记录锁;
向后扫描到age=30的记录,满足查询条件,因此会对该二级索引加一把(22,30]的next-key lock。
age=30的这条记录的主键索引id=20加一把记录锁。
age=30是最后一条记录,存储引擎会用一个特殊记录来标识最后一条记录,同时加一把(30,+∞]的 next-key lock
加锁的效果:
其他事务无法删除或更新 age=22的这行记录。对于是否能插入age=21 和age=22的新记录,需要根据id值来判断。
其他事务无法删除或更新 id=10 的这一行记录;
其他事务无法删除或更新 age=30的这行记录,其他事务无法插入age值范围为 23,24,25... 29的新记录。同时,age=22和age=30的新记录也无法插入。
其他事务无法删除或更新 id=20 的这一行记录;
其他事务无法插入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。这就相当于锁住了全表,其他事务对该表的增、删、改 都会被阻塞。生产环境中要尽量避免这类操作,否则会带来锁表,影响生产业务的情况发生。
【以上仅为个人观点,如有不同意见,欢迎留言讨论!】


点击蓝字 关注我们





