死锁概念
死锁是指两个及以上的事务在执行过程中因争锁造成的相互等待。
解决死锁一般有两种方法,
- 设置超时时间:两个事务等待过程实际上是阻塞过程,对参数
innodb_lock_wait_timeout
进行设定来设置超时时间。 - wait-for graph检验死锁,一旦发现存在死锁,则对死锁中的事务进行回滚。
死锁的解决
1)设置超时时间
前者的优势是简单,一旦两个事务出现死锁的情况,其中一个事务先到达设定的阈值后,该事务进行回滚,另一个事务可以继续执行。
但是这种方法也存在弊端,设置超时时间的方式可以看做是FIFO的顺序进行回滚,如果超时事务的操作很多,使用undo log进行回滚时的消耗可能比未超时的事务要大得多。
2)wait-for graph
wait-for graph可用于进行死锁检测,该方法也是InnoDB使用的死锁解决方法。wait-for graph保存两种信息,
- 锁的信息链表
- 事务等待链表
通过上述两个链表可以构造出一张图,如果图中存在回路,说明存在死锁。
wait-for graph中,图中的节点是事务,对节点之间边的定义如下,
- 事务T1等待事务T2释放锁
- 事务T1发生在事务T2之后
以下图为例,
上图包含的信息如下,
transaction wait lists
中存在4个事务,所以在wait-for graph中也会有四个节点。Lock lists
中,
事务t2对记录row1使用了X lock锁定该行数据。
事务t1对记录row2使用了S Lock锁定该行数据。
事务t1和t2的锁不兼容,所以t1在等待事务t2释放row1上的锁,故存在一条从t1指向t2的边
事务t2的X lock与任何锁都不兼容,所以t2在等待事务t1和t4释放记录row2上的锁,故存在从t2指向t1和t4的边
由此得到如下wait-for graph,
图中可以发现节点t1和t2直接存在回路,故这两个节点对应的事务存在死锁。
wait-for graph是主动监测死锁的机制,通常采用深度优先算法实现。一旦发现存在死锁,在两个事务中选择undo成本低的事务进行回滚。
死锁示例
死锁只存在于并发的情况,常见的死锁有两种。
1)AB-BA死锁
引发该类型死锁的原因是两个事务之间相互等待,
InnoDB默认大部分异常发生时不会回滚,但是死锁除外。发现死锁后,InnoDB会马上回滚undo成本低的事务。
2)特殊死锁
当前事务持有待插入记录的下一个记录的X锁,等待队列中存在一个S锁的请求,此时可能发生死锁。
create table t(
a int primary key;
)engine=InnoDB;
insert into t values(1), (2), (3), (4), (5);
上面的操作中,会话A已经持有记录4的X锁,会话A在插入记录3时,发生了死锁的情况。
会话B在会话A获取了记录4的X锁后,请求记录4的S锁,但是由于X锁的不兼容性,导致会话A的事务陷入阻塞。但是会话A此时已经争取到了记录1和2的S锁。
如果会话A的insert语句执行成功,会话B在获取到记录4的S锁后,还需要回过头去争取记录3的S锁。此时显得不合理,故InnoDB主动选择了死锁,并且回滚undo成本较高的事务,这一点与AB-BA死锁处理有差异。
降低死锁发生概率
- 更新数据时,
where
子句的条件字段尽量使用索引字段 - 使用索引字段时,尽量使用主键或唯一索引更新数据
- 减少非主键、非唯一索引上的范围更新
- 尽可能在事务中一次性锁定所有需要的数据行
- 对隔离性要求没那么严格时,将RR级别调整为RC级别,因为RR级别的间隙锁可能导致死锁
分析死锁的方法
实际工作中,死锁很难完全避免。捕获并处理死锁是编程时必要的。
InnoDB中使用show engine innodb status\G;
可以查看最后一个死锁的信息,
此外设置innodb_print_all_deadlocks=on
可以在error log中记录全部死锁信息。