记一次线上问题 → Deadlock 的分析与优化
开心一刻
今天女朋友很生气
女朋友:我发现你们男的,都挺单纯的
我:这话怎么说
女朋友:脑袋里就只想三件事,搞钱,跟谁喝点,还有这娘们真好看
我:你错了,其实我们男人吧,每天只合计一件事
女朋友:啥事呀?
我:这娘们真好看,得搞钱跟她喝点
问题复现
需求背景
MySQL8.0.30 ,隔离级别是默认的,也就是 REPEATABLE-READ
表: tbl_class_student ,id 非自增,整张表的全部字段数据都是从上游服务进行同步
需求:上游服务发送同步MQ,本服务收到消息后再调上游服务接口,查询全量数据,对 tbl_class_student 表数据进行更新,若记录存在则更新,不存在则插入
这需求是不是很明确?放心,没有下套!
线上问题
通过线上异常日志,最终定位到如下代码
咋一看,这代码是不是无比的清晰明了?
都不用注释,就能清楚的知道这个代码是在做什么:逐行更新,存在则更新,不存在则插入
是不是无比的契合需求?
但是,真的就完美无瑕吗
且看我表演一波
表演代码如下:
@Override @Transactional(rollbackFor = Exception.class) public void batchSaveOrUpdate(List<TblClassStudent> classStudents) { if(CollectionUtils.isEmpty(classStudents)) { return; } classStudents.forEach(classStudent -> { this.getBaseMapper().saveOrUpdate(classStudent); try { // 为了方便复现问题,睡眠1秒 TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } }); } // 单元测试 @Test public void batchSaveOrUpdateTest() throws InterruptedException { TblClassStudent classStudent = new TblClassStudent(); classStudent.setId(1); classStudent.setClassNo("20231010"); classStudent.setStudentNo("20231010201"); TblClassStudent classStudent1 = new TblClassStudent(); classStudent1.setId(2); classStudent1.setClassNo("20231010"); classStudent1.setStudentNo("20231010202"); List<TblClassStudent> classStudents1 = new ArrayList<>(); classStudents1.add(classStudent); classStudents1.add(classStudent1); List<TblClassStudent> classStudents2 = new ArrayList<>(); classStudents2.add(classStudent1); classStudents2.add(classStudent); // 模拟2个线程,同时批量更新 CountDownLatch latch = new CountDownLatch(2); new Thread(() -> { studentService.batchSaveOrUpdate(classStudents1); latch.countDown(); }, "t1").start(); new Thread(() -> { studentService.batchSaveOrUpdate(classStudents2); latch.countDown(); }, "t2").start(); latch.await(); System.out.println("主线程执行完毕"); }
Deadlock 就这么诞生了!
优化处理
死锁产生条件
死锁产生的条件,大家还记得吗?
回到上诉案例,锁的持有、申请情况如下
死锁自然就产生了
那么该如何处理了
排序处理
不同线程调用同一个方法处理数据而产生死锁
这种情况对处理的数据进行排序处理,使得不同线程申请数据库锁的顺序保持一致,那么就不会产生死锁
分批处理
事务时间越短越好
批量逐条更新,会导致事务持续的时间很长,那么出现死锁的概率就越大
分批处理可以减少事务时长
加锁处理
这里的锁指的并非数据库层面的锁,而是业务代码层面的锁
可以是 JVM 的锁,适用于单节点部署的情况
可以是分布式锁,适用于单节点部署,也适用于多节点部署;具体实现方式有很多,结合实际情况选择一种合适的实现方式即可
总结
1、批量逐条更新,这是严令禁止的
效率低下,导致事务时长大大增加,会引发一系列其他的问题
2、数据库的加锁是比较复杂的,不同的数据库的加锁实现也是有区别的
本篇中的死锁案例还是比较好分析的
遇到不好分析的,需要向同事(dba、开发同事等)发出求助,也可以线上求助数据库博主
3、面对不同问题,结合业务来分析出最合适的处理方式
有的业务对性能要求高
有的业务对数据准确性要求高