大家好,我是小林。昨天有位读者在美团二面时被问到幻读:面试官的意思大概是mysql记录锁+间隙锁可以防止删除操作导致的幻读?答案是肯定的。接下来我们通过几个小实验来证明这个结论,顺便帮大家回顾一下记录锁+间隙锁。什么是幻读?首先,我们看一下MySQL文档是如何定义PhantomRead的:所谓幻读问题,就是同一个查询在不同的时间产生不同的行集合,在一个事务中发生。翻译:当同一个查询在不同的时间出现所谓的幻象问题发生在事务中,当产生不同的结果集时。例如,假设一个事务在T1和T2分别执行了如下查询语句,没有执行任何其他语句:SELECT*FROMt_testWHEREid>100;只要T1时刻和T2时刻执行产生的结果集不一样,就会出现幻读问题,例如:T1时刻执行结果是5行记录,而T2时刻执行结果是6行行记录,则出现幻读问题。T1次执行的结果是有5行记录,而T2次执行的结果是有4行记录,这也是幻读的问题。MySQL如何解决幻读?MySQL可重复读隔离级别就是为了解决幻读问题。有两种查询数据的方式,所以解决方法不同:对于快照读(普通select语句),幻读是MVCC解决的,因为可以重复读隔离级别下,事务执行时看到的数据总是与交易开始时看到的数据一致。即使中间有一条数据被其他事务插入,也无法查询到这条数据,这样就很好的避免了幻读问题。对于当前读(select...forupdate等语句),通过next-keylock(recordlock+gaplock)解决幻读,因为select...forupdate语句执行时,next-keylock,如果另一个事务在next-key锁范围内插入一条记录,insert语句就会被阻塞,无法成功插入,这样就很好的避免了幻读的问题。实验验证接下来验证“MySQL记录锁+间隙锁可以防止删除操作导致的幻读问题”的结论。实验环境:MySQL8.0版本,可重复读隔离级别。现在有一张用户表(t_user),表中只有一个主键索引,表中有如下几行数据:现在一个事务A执行了一条查询语句,有6行用户年龄大于20岁。然后,事务B执行一条删除id=2的语句:此时,事务B的delete语句进入等待状态,表示无法删除。所以MySQL记录锁+间隙锁可以防止删除操作带来的幻读问题。锁分析的问题来了。事务A在执行select...forupdate语句时,加了什么样的锁?我们可以使用select*fromperformance_schema.data_locks\G;语句查看事务执行SQL时加了哪些锁。输出内容很多,一共11行信息,我删除了一些不重要的信息:从上面输出的信息可以看出,增加了两种不同粒度的锁,分别是:表锁(LOCK_TYPE:TABLE):X类型的意向锁;行锁(LOCK_TYPE:RECORD):X类型的下一键锁;这里我们主要关注“行锁”。图中LOCK_TYPE中的RECORD表示行级锁,不是记录锁:如果LOCK_MODE为X,表示是next-key锁;如果LOCK_MODE为X,REC_NOT_GAP,表示是记录锁;如果LOCK_MODE为X,GAP,表示是间隙锁;那么通过LOCK_DATA信息,可以确定next-keylock的范围,如何确定呢?根据我的经验,如果LOCK_MODE是next-key锁或者gap锁,那么LOCK_DATA代表锁范围最右边的值,锁范围最左边的值就是LOCK_DATA上一条记录的值。因此,此时事务A在主键索引(INDEX_NAME:PRIMARY)上增加了10个next-key锁,如下:X-typenext-keylocks,范围:(-∞,1]X-typenext-键锁,范围:(1,2]X型下一键锁,范围:(2,3]X型下一键锁,范围:(3,4]X型下一键锁,范围:(4,5]X型下一键锁,范围:(5,6]X型下一键锁,范围:(6,7]X型下一键锁,范围:(7,8]next-keylock,range:(8,9]X型next-keylock,range:(9,+∞]这个相当于给整张表加锁,其他事务在增删改查都会被阻塞当操作改变时,只有当事务A提交事务时,事务A执行过程中产生的锁才会被释放。为什么只有查询年龄超过20年才会对整张表加锁?是因为查询语句事务A的是全表扫描,在遍历索引时加锁,不为输出结果加锁。因此,在线执行update、delete、select...forupdate等加锁语句时,一定要检查语句是否使用了索引。如果是全表扫描,会在每个索引上加一个next-key锁,这相当于锁定了整个表,这是一个严重的问题。如果age被索引,那么事务A的查询会加什么锁呢?接下来,我在age字段上创建一个索引,然后执行这个查询语句:接下来,继续使用select*fromperformance_schema.data_locks\G;语句来检查在执行SQL事务期间添加了哪些锁。具体的信息我就不打印了,我就说结论吧。因为表中有两个索引,分别是主键索引和年龄索引,所以这两个索引会分别加锁。主键索引会加如下锁:X型记录锁,锁id=2的记录;X型记录锁,锁定id=3的记录;X型记录锁,锁定id=5的记录;X型记录锁,锁定id=6的记录;X型记录锁,锁定id=7的记录;X型记录锁,锁定id=8的记录;在分析ageindexlock的范围时,必须先对age字段进行排序。age索引加锁:X型next-key锁,锁年龄范围为(19,21]的记录;X型next-key锁,锁年龄范围为(21,21]的记录;X-typenext-keylock,锁定agerange(21,23]的记录;X-typenext-keylock,锁定agerange(23,23]的记录;X-typenext-keylock,锁定agerange(23,39]记录;X型next-key锁,锁年龄范围(39,43]记录;X型next-key锁,锁年龄范围(43,+∞]记录;化简,age索引next-keylock的取值范围是(19,+∞)。可见age字段被索引后,查询语句是索引查询,不会扫描全表,所以不会全表给Lock,综上所述,age字段被索引后,事务A执行如下查询语句后,主键索引和age索引会被锁住,如下图。事务A被锁定后,事务B、C、D、E在执行后面的语句时会被阻塞。综上所述,在MySQL的可重复读隔离级别下,“当前读”的查询语句会在索引上加上记录锁+间隙锁,避免其他事务执行“增删改查”时出现幻读现象。并修改”。需要注意的一点是,在执行update、delete、select...forupdate等带有锁属性的语句时,一定要检查语句是否已经到索引中去了。如果是全表扫描,next会被添加到每个索引中。-keylock相当于锁定了整个表,这是一个严重的问题。这次教大家如何分析具体加在事务上的锁。以后可以多做实验,自己尝试分析分析。掌握分析方法远比死记硬背加锁规则好!
