MySQL的行级锁是如何精确加在特定行的?

摘要:开篇结论 加锁的对象是索引,加锁的基本单位是 next-key lock,它是由记录锁和间隙锁组合而成的,next-key lock 是左开右闭区间,而间隙锁是左开右开区间。 在只使用记录锁或者间隙锁就能避免幻读现象的场景下, next-k
开篇结论 加锁的对象是索引,加锁的基本单位是 next-key lock,它是由记录锁和间隙锁组合而成的,next-key lock 是左开右闭区间,而间隙锁是左开右开区间。 在只使用记录锁或者间隙锁就能避免幻读现象的场景下, next-key lock 就会退化成记录锁或间隙锁。 假设这个表,id 是主键索引(唯一索引),age 是普通索引(非唯一索引),name 是普通的列。数据如下: 唯一索引等值查询 当查询的记录是「存在」的,在索引树上定位到这一条记录后,将该记录的索引中的 next-key lock 会退化成「记录锁」。 当查询的记录是「不存在」的,在索引树找到第一条大于该查询记录的记录后,将该记录的索引中的 next-key lock 会退化成「间隙锁」。 记录存在的情况 假设事务 A 执行了这条等值查询语句,查询的记录是存在于表中的。 select * from user where id = 1 for update; 那么,事务 A 会为 id 为 1 的这条记录就会加上 X 型的记录锁。 接下来,如果有其他事务,对 id 为 1 的记录进行更新或者删除操作的话,这些操作都会被阻塞 为什么这里退化为了记录锁? 原因在于这里仅靠记录锁也能避免幻读的问题。 当其他事务,对 id 为 1 的记录进行更新或者删除或插入id为1的数据的操作时,由于记录锁,这些操作都会被阻塞,也就不会出现前后两次查询的结果集不同,也就避免了幻读的问题。 记录不存在的情况 假设事务 A 执行了这条等值查询语句,查询的记录是不存在于表中的。 select * from user where id = 2 for update; 此时事务 A 在 id = 5 记录的主键索引上加的是间隙锁,锁住的范围是 (1, 5)。 接下来,如果有其他事务插入 id 值为 2、3、4 这一些记录的话,这些插入语句都会发生阻塞。 为什么这里退化为了间隙锁? 原因在于这里仅靠间隙锁也能避免幻读的问题。 如果这里加的是next-key lock,那就意味着其他事务无法更新/删除 id = 5 这条记录,但实际上即使更新/删除 id = 5 这条记录,也不会出现前后两次查询的结果集不同,查不到的还是查不到。 唯一索引范围查询 当唯一索引进行范围查询时,会对每一个扫描到的索引加 next-key 锁,然后如果遇到下面这些情况,会退化成记录锁或者间隙锁: 针对大于等于的范围查询,因为存在等值查询的条件,那么如果等值查询的记录是存在于表中,那么该记录的索引中的 next-key 锁会退化成记录锁。 针对小于或者小于等于的范围查询,要看条件值的记录是否存在于表中: 当条件值的记录不在表中,那么不管是小于还是小于等于条件的范围查询,扫描到终止范围查询的记录时,该记录的索引的 next-key 锁会退化成间隙锁,其他扫描到的记录,都是在这些记录的索引上加 next-key 锁。 当条件值的记录在表中,如果是小于条件的范围查询,扫描到终止范围查询的记录时,该记录的索引的 next-key 锁会退化成间隙锁,其他扫描到的记录,都是在这些记录的索引上加 next-key 锁;如果小于等于条件的范围查询,扫描到终止范围查询的记录时,该记录的索引 next-key 锁不会退化成间隙锁。其他扫描到的记录,都是在这些记录的索引上加 next-key 锁。 针对 大于或者大于等于 的范围查询 针对 大于 的范围查询的情况 假设事务 A 执行了这条范围查询语句: select * from user where id > 15 for update; 此时,事务 A 在主键索引上加了两个next-key 锁: 在 id = 20 这条记录的主键索引上,加了范围为 (15, 20] 的 next-key 锁,意味着其他事务即无法更新或者删除 id = 20 的记录,同时无法插入 id 值为 16、17、18、19 的这一些新记录。 在特殊记录( supremum pseudo-record)的主键索引上,加了范围为 (20, +∞] 的 next-key 锁,意味着其他事务无法插入 id 值大于 20 的这一些新记录。 这里没有next-key锁都没有退化。 原因在于需要保证 前后两次查询 id>15 的结果集相同。即需要保证 id>15不会出现新纪录(间隙锁),并且已存在的记录不改变(记录锁)。 针对 大于等于 的范围查询的情况。
阅读全文