8 MySQL 死锁

1 死锁的发现

左图那两辆车造成死锁了吗?不是!右图四辆车造成死锁了吗?是!

8 MySQL 死锁

innodb 引擎的 mysql 数据库会主动探知死锁,并回滚某个一直苦苦等待的事务。

innodb 如何探知死锁:当两个事务在相互等待时,当一个等待的时间超过设置的某一阀值时,对其中的一个事务进行回滚,另一个事务就能继续执行。在 innodb 中,参数 innodb_lock_wait_timeout 来设置超时时间。

1.1 wait-for graph 原理

我们怎么知道上图中的四辆车是死锁呢?它们相互等待对方的资源,而且形成了环路。我们将每辆车堪为一个节点,当节点 1 需要等待节点 2 的资源时,就生成了一条有向边指向节点 2 ,最后形成一个有向图。我们只要检测这个有向图是否出现环路即可,出现环路就是死锁。这就是 wait-for graph 算法。

8 MySQL 死锁

innodb 将各个事务看为一个个节点,资源就是各个事务占用的锁,当事务 1 需要等待事务 2 的锁时,就生成 一条有向边从 1 指向 2,最后形成一个有向图。

1.2 innodb 隔离级别、索引与锁

死锁检测是死锁发生时 innodb 给我们的救命稻草,我们需要它,但是我们更需要的是避免发生死锁的能力,如何尽可能的避免呢?这就需要了解 innodb 中的锁。

1.2.1 锁与索引的关系

假设我们有一张消息表(msg),里面有3个字段。假设id是主键,token是非唯一索引,message没有索引。

id: bigint
token: varchar(30)
message: varchar(4096)

innodb 对于主键使用了聚簇索引,这是一种数据存储方式,表数据是和主键一起存储,主键索引的叶结点存储行数据。对于普通索引,其叶子节点存储的是主键值。

8 MySQL 死锁

下面分析下索引和锁的关系:

  • delete from msg where id=2;

由于 id 是主键,因此直接锁住整行记录即可。

8 MySQL 死锁

  • delete from msg where token='cvs';

由于token是二级索引,因此首先锁住二级索引(两行),接着会锁住相应主键所对应的记录;

8 MySQL 死锁

  • delete from msg where message='订单号是多少';

message没有索引,所以走的是全表扫描过滤。这时表上的各个记录都将添加上X锁。

8 MySQL 死锁

1.2.2 锁与隔离级别的关系

为了保证并发操作数据的正确性,数据库都会有事务隔离级别的概念:

  • RU:未提交读(Read uncommitted)
  • RC:已提交读(Read committed)
  • RR:可重复读(Repeatable read)
  • Serializable:可串行化

我们较常用的是 RC 和 RR

  • 提交读(RC):只能读取到已经提交的数据。
  • 可重复读(RR):在同一个事务内的查询都是事务开始时刻一致的,InnoDB默认级别。

如下图所示,事务A在第一次查询时得到1条记录,在第二次执行相同查询时却得到两条记录。从事务A角度上看是见鬼了!这就是幻读,RC级别下尽管加了行锁,但还是避免不了幻读。

8 MySQL 死锁

innodb 的 RR 隔离级别可以避免幻读发生,怎么实现?当然需要借助于锁了!

为了解决幻读问题,innodb引入了gap锁。

在事务A执行:update msg set message=’订单’ where token=’asd’;

innodb 首先会和 RC 级别一样,给索引上的记录添加上X锁,此外,还在非唯一索引’asd’与相邻两个索引的区间加上锁。

这样,当事务B在执行insert into msg values (null, ‘asd’, ‘hello’); commit;时,会首先检查这个区间是否被锁上,如果被锁上,则不能立即执行,需要等待该gap锁被释放。这样就能避免幻读问题。

8 MySQL 死锁

3 死锁成因

了解了innodb锁的基本原理后,下面分析下死锁的成因。如前面所说,死锁一般是事务相互等待对方资源,最后形成环路造成的。下面简单讲下造成相互等待最后形成环路的例子。

3.1 不同表相同记录行锁冲突

这种情况很好理解,事务A和事务B操作两张表,但出现循环等待锁情况。

8 MySQL 死锁

3.2 相同表记录行锁冲突

这种情况比较常见,之前遇到两个job在执行数据批量更新时,jobA处理的的id列表为[1,2,3,4],而job处理的id列表为[8,9,10,4,2],这样就造成了死锁。

8 MySQL 死锁

3.3 不同索引锁冲突

这种情况比较隐晦,事务A在执行时,除了在二级索引加锁外,还会在聚簇索引上加锁,在聚簇索引上加锁的顺序是[1,4,2,3,5],而事务B执行时,只在聚簇索引上加锁,加锁顺序是[1,2,3,4,5],这样就造成了死锁的可能性。

8 MySQL 死锁

3.4 gap 锁冲突

innodb在RR级别下,如下的情况也会产生死锁,比较隐晦。不清楚的同学可以自行根据上节的gap锁原理分析下。

8 MySQL 死锁

4 如何尽可能避免死锁

  1. 以固定的顺序访问表和行。比如对第2节两个job批量更新的情形,简单方法是对id列表先排序,后执行,这样就避免了交叉等待锁的情形;又比如对于3.1节的情形,将两个事务的sql顺序调整为一致,也能避免死锁。
  2. 大事务拆小。大事务更倾向于死锁,如果业务允许,将大事务拆小。
  3. 在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁概率。
  4. 降低隔离级别。如果业务允许,将隔离级别调低也是较好的选择,比如将隔离级别从RR调整为RC,可以避免掉很多因为gap锁造成的死锁。
  5. 为表添加合理的索引。可以看到如果不走索引将会为表的每一行记录添加上锁,死锁的概率大大增大。