死锁场景总结

Posted 机械公敌

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了死锁场景总结相关的知识,希望对你有一定的参考价值。

1、死锁案例引入

1.1背景简介

在进行数据库切换(Oracle->MySQL)过程中,涉及存量数据迁移和增量数据同步工作。其中,在存量数据迁移过程中,发生了死锁。

1.2死锁发生场景

1.2.1表索引介绍

索引名称

索引字段

是否唯一索引

PRIMARY

ID

UK_A_B

A, B

其他二级索引

……

1.2.2业务流程

//开启多线程,以下代码逻辑为单个线程内的处理逻辑

// 分页拉取旧库单据信息(根据主键排序)

// 查询新库中已存在的数据

// 删除新库中已存在单据

//插入新数据

1.2.3代码提问

Q:不同线程是否会拉取到重复的数据?

A:数据拉取是根据主键有序拉取,且经过实际验证,不存在重复数据。因此不存在不同线程间竞争同一行锁且存在互斥的情况。

 
Q:单据删除操作,是否会出现间隙锁,从而触发间隙锁和插入意向锁冲突?

A:数据删除操作是基于唯一约束等值删除,且删除前会进行数据查询,只有该条数据存在时才会进行删除,因此理论上不会有间隙锁。

 

Q:有没有可能是查询时数据存在,但在删除之前被其他业务删除了?

A:数据删除均为逻辑删除,不会物理删除。而且在每次完整的数据迁移过程中,必定概率性出现死锁,因此排除该原因。(百万量级数据,分页参数500,每次迁移出现死锁概率/次数不等)

2、温故知新

2.1锁类型及锁模式

 

锁类型(lock_type,锁的粒度)

锁模式

(lock_mode)

 

表锁

行锁

读锁

LOCK_IS

LOCK_S

写锁

LOCK_IX

LOCK_X

自增锁

LOCK_AUTO_INC

 

2.2行锁及其兼容矩阵

显式锁

隐式锁

Record Locks

Gap Locks

Next-Key Locks

Insert Intention Locks

 

 

Record Locks

Gap Locks

Next-Key Locks

Insert Intention Locks

Record Locks

 

 

Gap Locks

Next-Key Locks

 

 

Insert Intention Locks

   

(第一行表示已有的锁,第一列表示要加的锁)

2.3隐式锁转换

  1. InnoDB 在插入记录时,是不加锁的。如果事务 A 插入记录且未提交,这时事务 B 尝试对这条记录加锁,事务 B 会先去判断记录上保存的事务 id 是否活跃,如果活跃的话,那么就帮助事务 A 去建立一个锁对象,然后自身进入等待事务 A 状态,这就是所谓的隐式锁转换为显式锁
  2. 前边说 INSERT 语句一般情况下不加锁,不过如果即将插入的间隙已经被其他事务加了 Gap Locks ,那么本次 INSERT 操作会阻塞,并且当前事务会在该间隙上加一个Insert Intention Locks,进入锁等待 。除此之外,在下边两种特殊情况下也会进行加锁操作:
    • 遇到duplicate key
    • 外键检查 
    1. 当子表中的外键值可以在父表中找到时,那么无论当前事务是什么隔离级别,只需要给父表中 对应的记录添加一个 S型正经记录锁 就好了。
    2. 当子表中的外键值在父表中找不到时:那么如果当前隔离级别不大于RC时,不对父表记录加 锁;当隔离级别不小于RR时,对父表中该外键值所在位置的下一条记录添加gap锁。 

2.4唯一约束冲突加锁

  • 如果是主键值重复,那么:

  1. 当隔离级别不大于RC时,插入新记录的事务会给已存在的主键值重复的聚簇索引记录添加S型Record Locks。
  2. 当隔离级别不小于RR时,插入新记录的事务会给已存在的主键值重复的聚簇索引记录添加S型Next-Key Locks。
  • 如果是唯一二级索引列重复,那么:

不论是哪个隔离级别,插入新记录的事务都会给已存在的二级索引列值重复的二级索引记录添加S型next-key锁,再强调一遍,加的是Next-Key Locks!加的是Next-Key Locks!加的是Next-Key Locks!这是RC隔离级别中为数不多的给记录添加gap锁的场景。

2.5加锁原则及案例分析

  1. 加锁基本单位是Next-Key Locks;
  2. 查找过程中访问到的对象才会加锁;
  3. 索引上的等值查询,给唯一索引加锁的时候,Next-Key Locks退化为记录锁;
  4. 索引上的等值查询,向右遍历到最后一个不满足等值条件时候,退化为间隙锁;
  5. 唯一索引上的查询范围会访问到不满足第一个值为止。
CREATE TABLE `Test` (
  `id` int(11) NOT NULL,
  `a` int(11) DEFAULT NULL,
  `b` int(11) DEFAULT NULL
  PRIMARY KEY (`id`),
  KEY `a` (`a`)
) ENGINE=InnoDB;

insert into Test values(0,0,0),(5,5,5),(10,10,10),(15,15,15);
  • 案例一:主键索引等值间隙锁

    • delete from Test where id=8;

      表中没有id=8这条记录,根据原则1,事务A加锁范围是(5,10],根据原则4是id=8是等值查询,遍历到id=10不满足查询提交,临建锁退化为间隙锁,所以最终加锁范围是(5,10)

  • 案例二:主键索引范围锁

    • delete from Test where id>=10 and id<11;

      根据原则1找到第一个id=10的行,next-key lock(5,10],根据原则3,主键id等值条件退化为了记录锁,只加了10这一行记录锁;

      范围查找继续往后找,找到id=15这一行,加临建锁(10,15]

  • 案例三:非唯一索引范围锁

    • delete from Test where a>=10 and a<11;

      根据原则1找到第一个a=10的行,next-key lock(5,10],(10,15]

  • 案例四:唯一索引范围锁

    • delete from Test where id>5 and id<=11;

      根据原则1和原则5,索引id的(5,10],(10,15]都会加锁

  • 案例五:非唯一索引等值锁

    • delete from Test where a=10;

      根据原则1和原则4,索引a的(5,10],(10,15)都会加锁

3、死锁案例分析

3.1场景分析

  • 在同一事务中:

  1. 先删除一条数据,则在该行数据上加Delete Mark,表示该行数据已经被删除,但并没有物理删除。

  2. 插入一条具有相同唯一约束的数据,触发Duplicate Key检查。

  3. 在打了Delete Mark的行数据上加Next-Key Locks。

  • 多线程场景下,不同线程操作数据的唯一约束是无序的,从而引发了Gap Locks和Insert Intention Locks冲突。同时,由于每个线程是批量操作多条数据,导致循环等待场景发生,引发死锁。

3.2解决方案

导致问题的根本原因是Gap Locks和Insert Intention Locks冲突,解决该问题有以下几个途径:

  1. 每次只操作单条数据。

    结论:可能导致锁等待,但是不会引发死锁。功能实现上可行,但是效率低,不推荐

  2. 将删除和插入动作拆分为两个步骤,独立进行。

    结论:删除和插入动作不是同一个事务,可能导致数据遗漏或产生脏数据,不推荐

  3. 取数逻辑按照唯一索引UK_BIZCODE_VERSION有序获取,保证不同线程对数据有序加锁。

    结论:可能引发锁等待,但不会引发死锁。但是,由于取数过程中,可能有新数据插入,而新插入数据在唯一约束中的位置是随机的,可能导致不同线程获取到重复数据,不可行

  4. 将唯一约束去掉,UK_BIZCODE_VERSION改为非唯一索引。

    结论:数据迁移过程中不会引发死锁,待完成迁移后将UK_BIZCODE_VERSION改为唯一索引,采用该方式

4、死锁案例扩展

案例一

https://github.com/aneasystone/mysql-deadlocks

案例二

  1. T1先插入name值为 g关羽 的记录,可以插入成功,此时对应的唯一二级索引记录被隐式锁保护,没有行锁。
  2. 接着T2也插入name值为 g关羽 的记录。由于T1已经插入name值为 g关羽 的记录,所以T2在插入二级索引记录时会遇到重复的唯一二级索引列值,此时T2想获取一个S型Next-Key Locks,但是T1并未提交,T1插入的name值为 g关羽 的记录上的隐式锁相当于一个X型Record Locks,所以T2向获取S型Next-Key Locks时会遇到锁冲突,T2进入阻塞状态,并且将T1的隐式锁转换为显式锁(就是帮助T1实现隐式锁转换)。
  3. 接着T1再插入一条name值为 d邓艾 的记录,被T2的Next-Key Locks阻塞。(…………………………………………………………)

Tips:只要别的事务生成了一个显式的gap锁的锁结构,不论那个事务已经获取到了该锁(granted),还是正在等待获取(waiting),当前事务的INSERT操作都应该被阻塞。

 

Q:如何解决这个场景下的问题呢?

A:方式一:一个事务只插入一条数据。方式二:改变插入顺序,使得唯一索引从小到大升序插入。

 

案例三

更新聚簇索引和二级索引时触发锁循环等待。(参考淘宝数据库内核月报:http://mysql.taobao.org/monthly/

  • 表test索引结构:

索引名称

索引字段

是否唯一索引

PRIMARY

ID

IDX_A

A

TX1

TX2

update test set a=\'a\' where id = 1;

update test set b=\'b\' where a=\'a\';

  • 案例解析:
  1. 上述案例中TX1走的索引是PRIMARY,而且涉及到二级索引字段的更新,因此先对聚簇索引加锁,再对二级索引加锁(如果二级索引字段没有被更新则没有该步骤)。

  2. TX2走的索引是IDX_A,且涉及到其他字段的更新需要更新聚簇索引,因此先对二级索引加锁,再对聚簇索引加锁。

上述案例中,死锁概率性发生,非稳定复现。

 

5、如何避免死锁

  1. 以固定的顺序访问表和行。在程序以批量方式处理数据的时候,如果事先对数据排序,保证每个线程按固定的顺序来处理记录,也可以大大降低出现死锁的可能;

  2. Gap 锁往往是程序中导致死锁的真凶,由于默认情况下 MySQL 的隔离级别是 RR,所以如果能确定幻读和不可重复读对应用的影响不大,可以考虑将隔离级别改成 RC,基本可以避免 Gap 锁导致的死锁;

  3. 为表添加合理的索引,如果不走索引将会为表的每一行记录加锁,死锁的概率就会大大增大;

  4. 避免大事务,尽量将大事务拆成多个小事务来处理;因为大事务占用资源多,耗时长,与其他事务冲突的概率也会变高;

  5. 我们知道 MyISAM 只支持表锁,它采用一次封锁技术来保证事务之间不会发生死锁,所以,我们也可以使用同样的思想,在事务中一次锁定所需要的所有资源,减少死锁概率;

  6. 避免在同一时间点运行多个对同一表进行读写的脚本,特别注意加锁且操作数据量比较大的语句;我们经常会有一些定时脚本,避免它们在同一时间点运行;

  7. 设置锁等待超时参数:innodb_lock_wait_timeout,这个参数并不是只用来解决死锁问题,在并发访问比较高的情况下,如果大量事务因无法立即获得所需的锁而挂起,会占用大量计算机资源,造成严重性能问题,甚至拖跨数据库。我们通过设置合适的锁等待超时阈值,可以避免这种情况发生。

6、参考文档
博客:
 
github:
 
书籍:
《MySQL是怎样运行的》
《我们都是小青蛙MySQL系列文章》掘金小册 

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

在上一篇文章《锁的类型以及加锁原理》主要总结了 MySQL 锁的类型和模式以及基本的加锁原理,今天我们就从原理走向实战,分析常见 SQL 语句的加锁场景。了解了这几种场景,相信小伙伴们也能举一反三,灵活地分析真实开发过程中遇到的加锁问题。

如下图所示,数据库的隔离等级,SQL 语句和当前数据库数据会共同影响该条 SQL 执行时数据库生成的锁模式,锁类型和锁数量

下面,我们会首先讲解一下隔离等级、不同 SQL 语句 和 当前数据库数据对生成锁影响的基本规则,然后再依次具体 SQL 的加锁场景。

隔离等级对加锁的影响

MySQL 的隔离等级对加锁有影响,所以在分析具体加锁场景时,首先要确定当前的隔离等级

  • 读未提交(Read Uncommitted 后续简称 RU):可以读到未提交的读,基本上不会使用该隔离等级,所以暂时忽略。
  • 读已提交(Read Committed 后续简称 RC):存在幻读问题,对当前读获取的数据加记录锁
  • 可重复读(Repeatable Read 后续简称 RR):不存在幻读问题,对当前读获取的数据加记录锁,同时对涉及的范围加间隙锁,防止新的数据插入,导致幻读。
  • 序列化(Serializable):从 MVCC 并发控制退化到基于锁的并发控制,不存在快照读,都是当前读,并发效率急剧下降,不建议使用。

这里说明一下,RC 总是读取记录的最新版本,而 RR 是读取该记录事务开始时的那个版本,虽然这两种读取的版本不同,但是都是快照数据,并不会被写操作阻塞,所以这种读操作称为 快照读(Snapshot Read)

MySQL 还提供了另一种读取方式叫当前读(Current Read),它读的不再是数据的快照版本,而是数据的最新版本,并会对数据加锁,根据语句和加锁的不同,又分成三种情况:

  • SELECT ... LOCK IN SHARE MODE:加共享(S)锁
  • SELECT ... FOR UPDATE:加排他(X)锁
  • INSERT / UPDATE / DELETE:加排他(X)锁

当前读在 RR 和 RC 两种隔离级别下的实现也是不一样的:RC 只加记录锁,RR 除了加记录锁,还会加间隙锁,用于解决幻读问题

不同 SQL 语句对加锁的影响

不同的 SQL 语句当然会加不同的锁,总结起来主要分为五种情况:

  • SELECT ... 语句正常情况下为快照读,不加锁;
  • SELECT ... LOCK IN SHARE MODE 语句为当前读,加 S 锁;
  • SELECT ... FOR UPDATE 语句为当前读,加 X 锁;
  • 常见的 DML 语句(如 INSERT、DELETE、UPDATE)为当前读,加 X 锁;
  • 常见的 DDL 语句(如 ALTER、CREATE 等)加表级锁,且这些语句为隐式提交,不能回滚。

其中,当前读的 SQL 语句的 where 从句的不同也会影响加锁,包括是否使用索引,索引是否是唯一索引等等。

当前数据对加锁的影响

SQL 语句执行时数据库中的数据也会对加锁产生影响。

比如一条最简单的根据主键进行更新的 SQL 语句,如果主键存在,则只需要对其加记录锁,如果不存在,则需要在加间隙锁。

至于其他非唯一性索引更新或者插入时的加锁也都不同程度的受到现存数据的影响,后续我们会一一说明。

具体场景分析

具体 SQL 场景分析主要借鉴何登成前辈的《MySQL 加锁处理分析》文章和 aneasystone 的系列文章,在他们的基础上进行了总结和整理。

我们使用下面这张 book 表作为实例,其中 id 为主键,ISBN(书号)为二级唯一索引,Author(作者)为二级非唯一索引,score(评分)无索引。

UPDATE 语句加锁分析

下面,我们先来分析 UPDATE 相关 SQL 在使用较为简单 where 从句情况下加锁情况。其中的分析原则也适用于 UPDATE,DELETE 和 SELECT ... FOR UPDATE等当前读的语句。

聚簇索引,查询命中

聚簇索引就是 InnoDB 存储引擎下的主键索引,具体可参考《MySQL索引》

下图展示了使用 UPDATE book SET score = 9.2 WHERE ID = 10 语句命中的情况下在 RC 和 RR 隔离等级下的加锁,两种隔离等级下没有任何区别,都是对 ID = 10 这个索引加排他记录锁。

聚簇索引,查询未命中

下图展示了 UPDATE book SET score = 9.2 WHERE ID = 16 语句未命中时 RR 隔离级别下的加锁情况。

在 RC 隔离等级下,不需要加锁;而在 RR 隔离级别会在 ID = 16 前后两个索引之间加上间隙锁。

值得注意的是,间隙锁和间隙锁之间是互不冲突的,间隙锁唯一的作用就是为了防止其他事务的插入新行,导致幻读,所以加间隙 S 锁和加间隙 X 锁没有任何区别。

二级唯一索引,查询命中

下图展示了 UPDATE book SET score = 9.2 WHERE ISBN = \'N0003\' 在 RC 和 RR 隔离等级下命中时的加锁情况。

在 InnoDB 存储引擎中,二级索引的叶子节点保存着主键索引的值,然后再拿主键索引去获取真正的数据行,所以在这种情况下,二级索引和主键索引都会加排他记录锁。

二级唯一索引,查询未命中

下图展示了 UPDATE book SET score = 9.2 WHERE ISBN = \'N0008\' 语句在 RR 隔离等级下未命中时的加锁情况,RC 隔离等级下该语句未命中不会加锁。

因为 N0008 大于 N0007,所以要锁住 (N0007,正无穷)这段区间,而 InnoDB 的索引一般都使用 Suprenum Record 和 Infimum Record 来分别表示记录的上下边界。Infimum 是比该页中任何记录都要小的值,而 Supremum 比该页中最大的记录值还要大,这两条记录在创建页的时候就有了,并且不会删除。

所以,在 N0007 和 Suprenum Record 之间加了间隙锁。

为什么不在主键上也加 GAP 锁呢?欢迎留言说出你的想法。

二级非唯一索引,查询命中

下图展示了 UPDATE book SET score = 9.2 WHERE Author = \'Tom\' 语句在 RC 隔离等级下命中时的加锁情况。

我们可以看到,在 RC 等级下,二级唯一索引和二级非唯一索引的加锁情况是一致的,都是在涉及的二级索引和对应的主键索引上加上排他记录锁。

但是在 RR 隔离等级下,加锁的情况产生了变化,它不仅对涉及的二级索引和主键索引加了排他记录锁,还在非唯一二级索引上加了三个间隙锁,锁住了两个 Tom 索引值相关的三个范围。

那为什么唯一索引不需要加间隙锁呢?间隙锁的作用是为了解决幻读,防止其他事务插入相同索引值的记录,而唯一索引和主键约束都已经保证了该索引值肯定只有一条记录,所以无需加间隙锁。

需要注意的是,上图虽然画着 4 个记录锁,三个间隙锁,但是实际上间隙锁和它右侧的记录锁会合并成 Next-Key 锁。

所以实际情况有两个 Next-Key 锁,一个间隙锁(Tom60,正无穷)和两个记录锁。

二级非唯一索引,查询未命中

下图展示了 UPDATE book SET score = 9.2 WHERE Author = \'Sarah\' 在 RR 隔离等级下未命中的加锁情况,它会在二级索引 Rose 和 Tom 之间加间隙锁。而 RC 隔离等级下不需要加锁。

无索引

当 Where 从句的条件并不使用索引时,则会对全表进行扫描,在 RC 隔离等级下对所有的数据加排他记录锁。在RR 隔离等级下,除了给记录加锁,还会对记录和记录之间加间隙锁。和上边一样,间隙锁会和左侧的记录锁合并成 Next-Key 锁。

下图就是 UPDATE book SET score = 9.2 WHERE score = 22 语句在两种隔离等级下的加锁情况。

聚簇索引,范围查询

上面介绍的场景都是 where 从句的等值查询,而范围查询的加锁又是怎么样的呢?我们慢慢来看。

下图是 UPDATE book SET score = 9.2 WHERE ID <= 25 在 RC 和 RR 隔离等级下的加锁情况。

RC 场景下与等值查询类似,只会在涉及的 ID = 10,ID = 18 和 ID = 25 索引上加排他记录锁。

而在 RR 隔离等级下则有所不同,它会加上间隙锁,和对应的记录锁合并称为 Next-Key 锁。除此之外,它还会在(25, 30] 上分别加 Next-Key 锁。这一点是十分特殊的,具体原因还需要再探究。

二级索引,范围查询

下图展示了 UPDATE book SET ISBN = N0001 WHERE score <= 7.9 在 RR 级别下的加锁情况。

修改索引值

UPDATE 语句修改索引值的情况可以分开分析,首先 Where 从句的加锁分析如上文所述,多了一步 Set 部分的加锁。

下图展示了 UPDATE book SET Author = \'John\' WHERE ID = 10 在 RC 和 RR 隔离等级下的加锁情况。除了在主键 ID 上进行加锁,还会对二级索引上的 Bob(就值) 和 John(新值) 上进行加锁。

DELETE 语句加锁分析

一般来说,DELETE 的加锁和 SELECT FOR UPDATE 或 UPDATE 并没有太大的差异。

因为,在 MySQL 数据库中,执行 DELETE 语句其实并没有直接删除记录,而是在记录上打上一个删除标记,然后通过后台的一个叫做 purge 的线程来清理。从这一点来看,DELETE 和 UPDATE 确实是非常相像。事实上,DELETE 和 UPDATE 的加锁也几乎是一样的。

INSERT 语句加锁分析

接下来,我们来看一下 Insert 语句的加锁情况。

Insert 语句在两种情况下会加锁:

  • 为了防止幻读,如果记录之间加有间隙锁,此时不能 Insert;
  • 如果 Insert 的记录和已有记录造成唯一键冲突,此时不能 Insert;

除了上述情况,Insert 语句的锁都是隐式锁。隐式锁是 InnoDB 实现的一种延迟加锁的机制来减少加锁的数量。

隐式锁的特点是只有在可能发生冲突时才加锁,减少了锁的数量。另外,隐式锁是针对被修改的 B+Tree 记录,因此都是记录类型的锁,不可能是间隙锁或 Next-Key 类型。

具体 Insert 语句的加锁流程如下:

  • 首先对插入的间隙加插入意向锁(Insert Intension Locks)

    • 如果该间隙已被加上了间隙锁或 Next-Key 锁,则加锁失败进入等待;
    • 如果没有,则加锁成功,表示可以插入;
  • 然后判断插入记录是否有唯一键,如果有,则进行唯一性约束检查

    • 如果不存在相同键值,则完成插入

    • 如果存在相同键值,则判断该键值是否加锁

      • 如果没有锁, 判断该记录是否被标记为删除

        • 如果标记为删除,说明事务已经提交,还没来得及 purge,这时加 S 锁等待;
        • 如果没有标记删除,则报 duplicate key 错误;
      • 如果有锁,说明该记录正在处理(新增、删除或更新),且事务还未提交,加 S 锁等待;

  • 插入记录并对记录加 X 记录锁;

后记

本文中讲解的 SQL 语句都是十分简单的,当 SQL 语句包含多个查询条件时,加锁的分析过程就往往更加复杂。我们需要使用 MySQL 相关的工具进行分析,并且有时甚至需要查询 MySQL 相关的日志信息来了解到底语句加了什么锁或者为什么产生死锁,下篇文章中我们就主要了解一下这些内容,请大家持续关注。

个人博客,欢迎来玩

以上是关于死锁场景总结的主要内容,如果未能解决你的问题,请参考以下文章

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

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

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

死锁处理

这样分析一个死锁问题

15.操作系统死锁处理