问题复现
需求背景
MySQL8.0.30 ,隔离级别是默认的,也就是 REPEATABLE-READ
表: tbl_class_student ,id 非自增,整张表的全部字段数据都是从上游服务进行同步
需求:上游服务发送同步MQ,本服务收到消息后再调上游服务接口,查询全量数据,对 tbl_class_student 表数据进行更新,若记录存在则更新,不存在则插入
这需求是不是很明确?放心,没有下套!
线上问题
通过线上异常日志,最终定位到如下代码
咋一看,这代码是不是无比的清晰明了?
都不用注释,就能清楚的知道这个代码是在做什么:逐行更新,存在则更新,不存在则插入
是不是无比的契合需求?
但是,真的就完美无瑕吗
且看我表演一波
表演代码如下:
代码语言:javascript复制@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、面对不同问题,结合业务来分析出最合适的处理方式
有的业务对性能要求高
有的业务对数据准确性要求高