当前位置: 首页 > 科技观察

MySQL死锁系列-常见加锁场景分析

时间:2023-03-16 11:03:07 科技观察

本文将从原理走向实战,分析常见SQL语句的加锁场景。了解了这些场景后,相信小伙伴们可以举一反三,灵活分析实际开发过程中遇到的加锁问题。如下图所示,数据库的隔离级别、SQL语句和当前数据库数据会共同影响SQL执行时数据库产生的锁模式、锁类型和锁数量。下面,我们先讲解隔离级别的基本规则,不同的SQL语句,以及当前数据库数据对锁产生的影响,然后依次介绍具体的SQL加锁场景。ImpactofisolationlevelonlockingMySQL的隔离级别对加锁有影响,所以在分析具体的加锁场景时,首先要确定当前的隔离级别。ReadUncommitted(ReadUncommitted以下简称RU):可以读取未提交的读,基本上不会用到这个隔离级别,暂时忽略。ReadCommitted(以下简称RC):存在幻读问题,对当前读得到的数据加了记录锁。RepeatableRead(可重复读以下简称RR):不存在幻读问题。在当前读得到的数据上加记录锁,在涉及的范围上加间隙锁,防止插入新的数据,造成幻读。Serializable:从MVCC并发控制退化为基于锁的并发控制,没有快照读,都是当前读,并发效率急剧下降,不推荐使用。这里说明一下,RC总是读取最新版本的记录,而RR读取的是记录交易开始时的版本。两个读取的版本虽然不同,但都是快照数据,不会写入。阻塞,所以这种读操作称为快照读(SnapshotRead)。MySQL还提供了另一种读取方法,称为当前读取(CurrentRead),读取的不再是快照版本的数据,而是最新版本的数据,并且会锁定数据。根据语句和锁的不同,分为三种情况:SELECT...LOCKINSHAREMODE:加共享(S)锁SELECT...FORUPDATE:加排他(X)锁INSERT/UPDATE/DELETE:加排他(X)锁RR和RC两种隔离级别下当前读的实现也不同:RC只加记录锁,RR除了记录锁外加间隙锁,用于解决幻读问题.不同的SQL语句对加锁的影响不同的SQL语句当然会加不同的锁,归纳为五种情况:SELECT...语句一般是快照读,没有加锁;SELECT...LOCKINSHAREMODE语句是针对当前读的,加一个S锁;SELECT...FORUPDATE语句是针对当前读取的,加上一个X锁;普通的DML语句(如INSERT、DELETE、UPDATE)都是针对当前读的,加上一个X锁;普通的DDL语句(如ALTER、CREATE等)加上表级锁,并且这些语句是隐式提交的,不能回滚。其中,当前读取的SQL语句where子句的不同也会影响加锁,包括是否使用索引,索引是否为唯一索引等。当前数据对锁的影响数据库中的数据也会影响SQL语句执行时的锁。比如最简单的基于主键更新的SQL语句,如果主键存在,只需要给它加一个记录锁,如果不存在,则需要加一个间隙锁。至于其他的非唯一索引更新或者插入时的锁,也会不同程度的受到已有数据的影响,后面会一一说明。具体场景分析SQL具体场景分析主要借鉴了何登成前辈的《MySQL 加锁处理分析》文章和aneasystone的系列文章,并在此基础上进行了归纳整理。我们以下面的书表为例,其中id是主键,ISBN(书号)是二级唯一索引,Author(作者)是二级非唯一索引,score(分数)没有索引。UPDATE语句加锁分析下面我们先来分析一下UPDATE相关SQL在使用比较简单的where子句的情况下的加锁情况。分析原则也适用于当前读取的语句,例如UPDATE、DELETE和SELECT...FORUPDATE。聚簇索引,查询命中聚簇索引是InnoDB存储引擎下的主键索引,详见《MySQL索引》。下图显示了当UPDATEbookSETscore=9.2WHEREID=10语句被命中时,RC和RR隔离级别下的锁定。两种隔离级别下没有区别,ID=10的索引是exclusiveRecordlock。Clusteredindex,querymiss下图是UPDATEbookSETscore=9.2WHEREID=16statementmiss时RR隔离级别下的加锁情况。RC隔离级别下,不需要加锁;而在RR隔离级别,会在ID=16前后的两个索引之间加一个gaplock,值得注意的是gaplock和gaplock之间没有冲突。间隙锁的唯一作用是防止其他事务插入新行,造成幻读。所以加gapS锁和加gapX锁没有区别。.Secondaryuniqueindex,queryhit下图是RC和RR隔离级别下UPDATEbookSETscore=9.2WHEREISBN='N0003'hit时的锁情况。在InnoDB存储引擎中,二级索引的叶子节点存储的是主键索引的值,然后通过主键索引获取真正的数据行,所以在这种情况下,二级索引和主键索引都是将添加一个独占记录锁。Secondaryuniqueindex,querymiss下图是RR隔离级别下UPDATEbookSETscore=9.2WHEREISBN='N0008'statementmiss时的加锁情况,RC隔离级别下statementmiss不会加锁。因为N0008大于N0007,所以需要锁定区间(N0007,正无穷大),InnoDB索引一般使用SuprenumRecord和InfimumRecord分别表示记录的上下边界。Infimum是一个小于页面中任何一条记录的值,而Supremum大于页面中最大的记录。这两条记录在页面创建时就存在,不会被删除。所以在N0007和SuprenumRecord之间加了一个gaplock。为什么不在主键上加一个GAP锁呢?欢迎留言表达你的想法。二级非唯一索引,查询命中下图是RC隔离级别下UPDATEbookSETscore=9.2WHEREAuthor='Tom'语句命中时的锁情况。我们可以看到,在RC级别下,二级唯一索引和二级非唯一索引的加锁条件是一致的,在涉及到的二级索引和对应的主键索引上加了独占记录锁。但是在RR隔离级别下,加锁的情况发生了变化。它不仅在涉及的二级索引和主键索引上加了排他记录锁,还在非唯一二级索引上加了三个间隙锁。存在与两个Tom索引值相关的三个范围。那为什么唯一索引不需要加间隙锁呢?间隙锁的作用是解决幻读,防止其他事务插入具有相同索引值的记录。唯一索引和主键约束都保证了索引值必须只有一条记录,所以不需要加间隙锁。需要注意的是,虽然上图显示的是4个记录锁和3个间隙锁,但实际上间隙锁和它右边的记录锁会合并成一个Next-Key锁。所以实际情况有两个Next-Key锁,一个gap锁(Tom60,positiveinfinity)和两个record锁。Secondarynon-uniqueindex,querymiss下图是UPDATEbookSETscore=9.2WHEREAuthor='Sarah'在RR隔离级别misslock的情况下,会在二级索引Rose和TomLock之间增加一个gap。但是,在RC隔离级别下,不需要锁定。无索引当Where子句的条件没有使用索引时,会扫描全表,并对RC隔离级别下的所有数据加排他记录锁。在RR隔离级别下,除了对记录加锁外,还会在记录与记录之间加一个间隙锁。如上,gap锁会和左边的record锁合并,形成Next-Key锁。下图是UPDATEbookSETscore=9.2WHEREscore=22语句在两种隔离级别下的加锁情况。聚簇索引,范围查询上面介绍的场景都是where子句的等价查询,那么范围查询的加锁呢?让我们慢慢来。下图是RC和RR隔离级别下UPDATEbookSETscore=9.2WHEREID<=25的加锁情况。与RC场景下的等价查询类似,只会对ID=10、ID=18、ID=25涉及的索引加排他记录锁,RR隔离级别下有所不同。它将添加一个间隙锁,它与相应的记录锁组合在一起,称为Next-Key锁。另外,它还会分别在(25,30]上加Next-Key锁,这个很特别,具体原因有待进一步探讨。二级索引,范围查询下图是UPDATEbookSETISBN=N0001WHEREscore<=7.9在RR级别的加锁情况。修改索引值修改索引值的UPDATE语句可以单独分析。首先,Where子句的加锁分析如上所述,多了一步加锁Set部分。下图是在RC和RR隔离级别下对UPDATEbookSETAuthor='John'WHEREID=10的加锁。除了在主键ID上加锁外,还在二级索引上加锁Bob(旧值)和John(新值)。DELETE语句锁分析一般来说,DELETE锁和SELECTFORUPDATE或UPDATE没有太大区别。因为,在MySQL数据库中,DELETE语句的执行并不是直接删除记录,而是在记录上打上删除标记,然后通过后台一个叫做purge的线程进行清理。从这一点来看,DELETE和UPDATE确实很相似。其实DELETE和UPDATE的加锁几乎是一样的。INSERT语句锁分析接下来我们看一下Insert语句的锁情况。Insert语句在两种情况下会被加锁:为了防止幻读,如果记录之间存在间隙锁,此时Insert不能插入;如果Insert的记录和已经存在的记录造成唯一键冲突,此时Insert不能插入;在其他情况下,Insert语句的锁都是隐式锁。隐式加锁是InnoDB为了减少加锁次数而实现的延迟加锁机制。隐式锁的特点是只有在可能发生冲突的时候才加锁,减少了加锁的次数。另外,隐式锁是针对修改后的B+Tree记录的,所以都是记录型锁,不能是间隙锁,也不能是Next-Key类型。具体Insert语句的加锁过程如下:首先,在insertedgap上添加InsertIntensionLocks(插入强度锁)。如果gap已经加了gap锁或者Next-Key锁,则加锁失败,进入wait;如果没有,那么如果加锁成功,说明可以插入;然后判断插入的记录是否有唯一键,如果有则进行唯一约束检查??;如果没有相同的键值,则完成插入;如果有相同的键值,则判断该键值是否被锁定,如果没有锁定,则判断记录是否被标记为删除。如果标记为已删除,则表示交易已经提交,还没来得及清除。此时加S锁等待;如果没有删除标记,则报重复键错误;如果有锁,表示正在处理记录(增删改查),事务还没有提交,加S锁等待;插入记录,给记录加X记录锁;后记本文讲解的SQL语句非常简单,当SQL语句包含多个查询条件时,加锁的分析过程往往比较复杂。我们需要借助MySQL相关的工具进行分析,有时甚至需要查询MySQL相关的日志信息,了解语句加了哪些锁,或者为什么会出现死锁。