一.问题背景
1.什么是死锁(Deadlock)
摘抄网上死锁的定义:是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。由于资源占用是互斥的,当某个进程提出申请资源后,使得有关进程在无外力协助下,永远分配不到必需的资源而无法继续运行,这就产生了一种特殊现象死锁。
简而言之,就是我和你都拿到某个资源,我需要等你手中的资源释放以后我才能去干接下来的活,你也需要等我手中的资源释放之后才能去继续干接下来的活,这样就好了,咱俩就一直等着吧,就形成了死锁,如果没有外力打破这种互相等待的尴尬,那么死锁就会一直持续下去。
2.形成死锁的条件
这一段也就在网上找的解释,大家看看就好,理解就行
(1)互斥条件:进程对所分配到的资源不允许其他进程进行访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源
(2)请求和保持条件:进程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他进程占有,此事请求阻塞,但又对自己获得的资源保持不放
(3)不可剥夺条件:是指进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放
(4)环路等待条件:是指进程发生死锁后,必然存在一个进程--资源之间的环形链
3.Mysql死锁
好的,介绍了一下死锁的相关知识,咱们进入一下正题,数据库死锁。数据库为什么发生死锁,发生数据库死锁有什么前提条件是什么呢?
首先,数据库发生死锁的前提,多个事务都持有同一种不互斥(互斥排他锁是不能同时存在的)锁,还都在等待对方释放锁,继续执行,这样就造成了数据库的死锁。不过好在,Mysql自己实现一种死锁检测机制,如果监测到有死锁的存在时,mysql会主动中断一个事务,这个事务失败退出后,打破了死锁形成的必要条件,那么另外的事务就会继续执行下去。
综上,在解决Mysql死锁之前,我们必须要对Mysql的各种锁有了解,分析数据库语句执行的时候,Mysql会不会加上锁,会加哪些锁?(因为Mysql锁的知识比较复杂,如果下面说的有错误的话,请大家见谅)
二.Mysql锁知识介绍
在介绍锁之前,大家需要知道Mysql的四种隔离级别,隔离级别不同,锁也是不一样的,网上相关知识有很多,就不多说了,推荐大家读一篇博客:http://hedengcheng.com/?p=771,虽然大神对锁有了很详细的解读,但是每个人可能理解成都都不一样,下面就是个人的一些理解,本文都采用的是mysql默认的隔离级别,可重复可(RR)。
1.锁的分类(具体的一些定义,大家自己去查一下,了解一下)
1.全局锁(数据库级别,做逻辑备份使用)
2.表级锁(意向共享锁(IS)、意向排他锁(IX))
3.行锁(共享锁(S)、排他锁(X)、间隙锁(GAP)、next-lock-key(间隙锁加行锁))
全局锁一般接触的比较少,可能都是DBA进行数据库逻辑备份的时候用的,我没用过。而行锁的话,是有数据库引擎自己实现的,众所周知,mysql有俩种搜索引擎InnoDb和 MyISAM 而后者是不支持行锁的,所以下面的行锁分析都是按照InnoDb引擎的分析的,下面是各种锁的冲突关系示意图。
2.锁的两阶段提交
在 InnoDB 事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是两阶段锁协议。所以一般在进行事务的时候,需要把加锁多,锁粒度大的语句放在事务的最后执行。
3.快照读和当前读
在理解这个之前,我们得要了解Mysql的MVCC(多版本并发控制)这种机制相当于一种乐观锁的实现,根据不同时间点保存数据快照,同一个事务看到的快照是一样的。
快照读:读的是快照,无需加锁,普通的数据库查询语句
当前读:读的是当下的数据,需要进行加锁(删除、更新、插入等都属于)
3.Mysql并发死锁案例分析
首先,Mysql对加行锁有如下的一个规则:
行锁,锁的是索引的值。如果没有条件行没有索引的话,那么就会走隐藏的聚簇索引。有个特殊的就是,如果没有走索引条件的话,就会进行全局扫描,这时候就相当于加了表锁。
下面就分析一下真实遇到的并发死锁案例分析
建表语句如下:
CREATE TABLE `lock_test` (
`id` int NOT NULL,
`c` int DEFAULT NULL,
`d` int DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_id_c` (`id`,`c`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
遇到的这种情况,是由于程序并发执行,插入相同的一条sql语句,示意图如下:
下面就分析一下为什么会出现死锁:
T1时刻,会话A插入一条记录,插入之后在加上记录锁(相当于排他锁(X锁)),T2时刻,因为存在唯一键冲突,会加上读锁(S锁),在插入的时候,发现会话A持有X锁,获取锁阻塞,同理T3时刻,会话C也出现阻塞等待X锁的情况。T4时刻,会话A发生回滚,释放X锁,此时会话B想获得X锁,但发现会话C有S锁存在,俩者冲突,同理会话C也在等待会话B的S锁,所以俩者产生了死锁。
由于Mysql有自己的死锁检测机制和解决机制,会话C失败推出,会话B正常执行插入操作,至此,死锁分析结束。
四.死锁解决方案
由唯一索引引起的死锁问题,归根到底,是由于并发请求造成的,无非是由于前端重复请求或者是网络抖动产生的,下面就介绍俩种解决方案。
1.缓存去重(推荐)
因为是并发请求,所以打到后端的请求参数都一样,为了防止并发请求造成并发数据库请求,我们可以在service层加一层缓存,把重复的请求给过滤掉,确保只放入一个请求进入,伪代码如下:
String key = id+c+d;
if(redisClient.exits(key)){
//并发请求,直接返回
return;
}
//唯一请求,过期时间根据实际情况而定
redisClient.sexEx(key,value,expireTime);
//数据库插入操作
dao.insert
2.异常捕捉
因为Mysql自己有死锁检测和恢复机制,所以即使发生了死锁,肯定有一条插入请求会落库成功,所以这时候只需要把死锁异常给catch住,让程序继续运行下去,不崩即可,伪代码就不写了,加个try、catch就行了。
五 总结
Mysql锁的知识还是很多的,还需要继续深入地了解,比如说insert还需要插入意向锁等。