线上某服务时不时报出以下异常(大约一天二十屡次):“Deadlock found when trying to get lock;”。mysql
Oh, My God! 是死锁问题。尽管报错很少,对性能目前看来也无太大影响,但仍是须要解决,保不齐哪天成为性能瓶颈。面试
为了更系统的分析问题,本文将从死锁检测、索引隔离级别与锁的关系、死锁成因、问题定位这五个方面来展开讨论。算法
左图那两辆车形成死锁了吗?不是!右图四辆车形成死锁了吗?是!sql
咱们mysql用的存储引擎是innodb,从日志来看,innodb主动探知到死锁,并回滚了某一苦苦等待的事务。问题来了,innodb是怎么探知死锁的?
数据库
直观方法是在两个事务相互等待时,当一个等待时间超过设置的某一阀值时,对其中一个事务进行回滚,另外一个事务就能继续执行。这种方法简单有效,在innodb中,参数innodb_lock_wait_timeout用来设置超时时间。bash
仅用上述方法来检测死锁太过被动,innodb还提供了wait-for graph算法来主动进行死锁检测,每当加锁请求没法当即知足须要并进入等待时,wait-for graph算法都会被触发。markdown
咱们怎么知道上图中四辆车是死锁的?他们相互等待对方的资源,并且造成环路!咱们将每辆车看为一个节点,当节点1须要等待节点2的资源时,就生成一条有向边指向节点2,最后造成一个有向图。咱们只要检测这个有向图是否出现环路便可,出现环路就是死锁!这就是wait-for graph算法。并发
innodb将各个事务看为一个个节点,资源就是各个事务占用的锁,当事务1须要等待事务2的锁时,就生成一条有向边从1指向2,最后行成一个有向图。
性能
死锁检测是死锁发生时innodb给咱们的救命稻草,咱们须要它,但咱们更须要的是避免死锁发生的能力,如何尽量避免?这须要了解innodb中的锁。spa
假设咱们有一张消息表(msg),里面有3个字段。假设id是主键,token是非惟一索引,message没有索引。
innodb对于主键使用了聚簇索引,这是一种数据存储方式,表数据是和主键一块儿存储,主键索引的叶结点存储行数据。对于普通索引,其叶子节点存储的是主键值。
聚簇索引和二级索引
下面分析下索引和锁的关系。
(1)delete from msg where id=2;
因为id是主键,所以直接锁住整行记录便可。
(2)delete from msg where token=’ cvs’;
因为token是二级索引,所以首先锁住二级索引(两行),接着会锁住相应主键所对应的记录;
(3)delete from msg where message=订单号是多少’;
message没有索引,因此走的是全表扫描过滤。这时表上的各个记录都将添加上X锁。
大学数据库原理都学过,为了保证并发操做数据的正确性,数据库都会有事务隔离级别的概念:
咱们在上面谈论的实际上是RC隔离级别下的锁,它能够防止不一样事务版本的数据修改提交时形成数据冲突的状况,但当别的事务插入数据时可能会出现问题。
以下图所示,事务A在第一次查询时获得1条记录,在第二次执行相同查询时却获得两条记录。从事务A角度上看是见鬼了!这就是幻读,RC级别下尽管加了行锁,但仍是避免不了幻读。
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锁被释放。这样就能避免幻读问题。
了解了innodb锁的基本原理后,下面分析下死锁的成因。如前面所说,死锁通常是事务相互等待对方资源,最后造成环路形成的。下面简单讲下形成相互等待最后造成环路的例子。
这种状况很好理解,事务A和事务B操做两张表,但出现循环等待锁状况。
这种状况比较常见,以前遇到两个job在执行数据批量更新时,jobA处理的的id列表为[1,2,3,4],而job处理的id列表为[8,9,10,4,2],这样就形成了死锁。
这种状况比较隐晦,事务A在执行时,除了在二级索引加锁外,还会在聚簇索引上加锁,在聚簇索引上加锁的顺序是[1,4,2,3,5],而事务B执行时,只在聚簇索引上加锁,加锁顺序是[1,2,3,4,5],这样就形成了死锁的可能性。
innodb在RR级别下,以下的状况也会产生死锁,比较隐晦。不清楚的同窗能够自行根据上节的gap锁原理分析下。
下面以本文开头的死锁案例为例,讲下如何排查死锁成因。
(1)经过应用业务日志定位到问题代码,找到相应的事务对应的sql;
由于死锁被检测到后会回滚,这些信息都会以异常反应在应用的业务日志中,经过这些日志咱们能够定位到相应的代码,并把事务的sql给梳理出来。
start tran
1 deleteHeartCheckDOByToken
2 updateSessionUser
...
commit复制代码
此外,咱们根据日志回滚的信息发如今检测出死锁时这个事务被回滚。
(2)肯定数据库隔离级别。
执行select @@global.tx_isolation,能够肯定数据库的隔离级别,咱们数据库的隔离级别是RC,这样能够很大几率排除gap锁形成死锁的嫌疑;
(3)找DBA执行下show InnoDB STATUS看看最近死锁的日志。
这个步骤很是关键。经过DBA的帮忙,咱们能够有更为详细的死锁信息。经过此详细日志一看就能发现,与以前事务相冲突的事务结构以下:
start tran
1 updateSessionUser
2 deleteHeartCheckDOByToken
...
commit复制代码