原创:简洁编程(微信公众号ID:Snail18321245641或者直接搜索简洁编程),转载请保留此声明。
前言
最近正值五一假期,本是一个可以让人心情愉悦的日子,但是昨天晚上的一通电话彻底打断了我对假期的美好憧憬。
“我不做大哥好多年,我只爱冰冷的床沿。。。”
随着我的电话铃声响起,我接过电话,电话那头传来DBA的声音。
“小帅,快看下生产环境,数据库有个表死锁了”。
突如其来的问题,让我有点措手不及,好好的数据库怎么会死锁呢?我抓紧登上生产的服务器排查问题,原来是在多线程里面开启数据库事务,而且这个事务里面执行的是 DELETE 和 INSERT 语句。抓紧时间进行修改。
/**
* 保单数据落库\
*
* @param assetLoanInsurancepolicyList
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void syncLoan(List<AssetLoanInsurancepolicy> assetLoanInsurancepolicyList) {
if (CollectionUtils.isEmpty(assetLoanInsurancepolicyList)) {
return;
}
List<String> list = assetLoanInsurancepolicyList.stream().map(AssetLoanInsurancepolicy::getInstallmentOrder).collect(Collectors.toList());
Example example = new Example(AssetLoanInsurancepolicy.class);
example.createCriteria().andIn("installmentOrder", list);
assetLoanInsurancepolicyMapper.deleteByExample(example);
assetLoanInsurancepolicyList.forEach(t -> {
t.setDeleted(new Byte("0"));
t.setCreateTime(new Date());
t.setUpdateTime(new Date());
});
assetLoanInsurancepolicyMapper.insertList(assetLoanInsurancepolicyList);
}
找到这段代码之后,开始进行修改。经过一顿猛如虎的操作之后,问题解决了。
衍生出以下问题:
-
死锁问题怎么分析?
-
数据库的死锁是怎么产生的?
-
当数据库的死锁产生之后,怎么处理?
数据库的死锁
首先,什么是数据库的死锁?
mysql死锁是多个事务因竞争锁而造成的一种相互等待的僵局。
其次,死锁需要具备什么条件?
- 互斥:一个资源每次只能被一个进程使用,即在一段时间内某 资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
- 请求与保持:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
- 不可剥夺:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)。
- 循环等待:若干进程间形成首尾相接循环等待资源的关系
最后,项目中的死锁怎么产生的?
比如,线程A要处理一段数据库的逻辑:
delete from ti where id = 1;
insert into ti (name,age) values('张三',18);
线程B也是需要执行相同的数据库逻辑:
delete from ti where id = 2;
insert into ti (name,age) values('李四',19);
高并发情况下,如果事务A想要执行 INSERT 语句,就需要获取锁。但此时事务B获取到了锁,正在执行 DELETE 语句,这时事务A的 INSERT 就会进入堵塞等待,而事务B的 INSERT 语句也需要等待执行。此时如果事务B执行了INSERT语句,那么事务A就会进入死锁。
这么说有点抽象,我们画个图来看下:
我们可以看到这种相互等待的过程就造成了死锁。
处理死锁
在实际的项目中我们出现了死锁该怎么处理?
我们知道死锁的原因是因为大家相互等待对方释放资源,所以我们打破僵局就可以了。
- PLAN1:加锁
最直接的就是不让他们直接形成相互争抢的局面,按照顺序执行。
在执行代码之前线程之间先进行获取锁,得到锁的线程去执行业务代码,没有获取到锁的线程进入等待状态,直到获取锁才执行业务代码。
final static String LOCK = "LOCK_ABC";\
\
@Autowired\
private RedisUtils redisUtils;\
\
@Override\
@Transactional(rollbackFor = Exception.class)\
public void syncLoan(List<AssetLoanInsurancepolicy> assetLoanInsurancepolicyList) {\
//判断其他线程是否获取锁\
if (!redisUtils.hasKey(LOCK)) {\
return;\
}\
//设置锁并且设置超时时间避免死锁\
if (!redisUtils.setIfAbsent(LOCK, "value", TimeUnit.SECONDS, 60L)) {\
return;\
}\
try {\
//执行业务逻辑\
if (CollectionUtils.isEmpty(assetLoanInsurancepolicyList)) {\
return;\
}\
List<String> list = assetLoanInsurancepolicyList.stream().map(AssetLoanInsurancepolicy::getInstallmentOrder).collect(Collectors.toList());\
Example example = new Example(AssetLoanInsurancepolicy.class);\
example.createCriteria().andIn("installmentOrder", list);\
assetLoanInsurancepolicyMapper.deleteByExample(example);\
assetLoanInsurancepolicyList.forEach(t -> {\
t.setDeleted(new Byte("0"));\
t.setCreateTime(new Date());\
t.setUpdateTime(new Date());\
});\
assetLoanInsurancepolicyMapper.insertList(assetLoanInsurancepolicyList);\
} catch (Exception e) {\
e.printStackTrace();\
} finally {\
//删除锁\
redisUtils.delete(LOCK);\
}\
}
这种方法虽然可以让获取到锁的线程按照顺序执行,但是影响性能,因为没有获取锁的线程只能堵塞等待,会浪费时间片,在并发大的时候,对服务器的压力很大。
推荐指数:⭐⭐⭐
- PLAN2:降低事务的隔离级别
确定事务是否能在更低的隔离级别上运行。执行提交读允许事务读取另一个事务已读取(未修改)的数据,而不必等待第一个事务完成。使用较低的隔离级别(例如提交读)而不使用较高的隔离级别(例如可串行读)可以缩短持有共享锁的时间,从而降低了锁定争夺。
推荐指数:⭐⭐⭐⭐
总结
- 数据库的死锁是由于多个事务竞争同一个资源而陷入等待所造成的现象。
- 可以通过人为的将多个事务按照顺序进行执行,从而避免争夺资源造成死锁。
- 尽可能使用低级别的事务隔离机制. MySQL是一个很大的体系,需要不断的深入学习。
祝各位:
少年辛苦终事成, 莫向光阴惰寸功。
作者介绍
简洁编程专注于java领域干货分享,提供大家容易接受的干货知识。面试、数据库、微服务、springBoot、高并发等相关知识,更多干货内容微信搜索【Snail18321245641或者简洁编程】,联系个人请搜索【janjbc18321245641】期待与您一起共同进步!