开心一刻
今天女朋友很生气
女朋友:我发现你们男的,都挺单纯的
我:这话怎么说
女朋友:脑袋里就只想三件事,搞钱,跟谁喝点,还有这娘们真好看
我:你错了,其实我们男人吧,每天只合计一件事
女朋友:啥事呀?
我:这娘们真好看,得搞钱跟她喝点
问题复现
需求背景
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("主线程执行完毕");
- }
复制代码 View Code Deadlock 就这么诞生了!
优化处理
死锁产生条件
死锁产生的条件,大家还记得吗?
回到上诉案例,锁的持有、申请情况如下
死锁自然就产生了
那么该如何处理了
排序处理
不同线程调用同一个方法处理数据而产生死锁
这种情况对处理的数据进行排序处理,使得不同线程申请数据库锁的顺序保持一致,那么就不会产生死锁
分批处理
事务时间越短越好
批量逐条更新,会导致事务持续的时间很长,那么出现死锁的概率就越大
分批处理可以减少事务时长
加锁处理
这里的锁指的并非数据库层面的锁,而是业务代码层面的锁
可以是 JVM 的锁,适用于单节点部署的情况
可以是分布式锁,适用于单节点部署,也适用于多节点部署;具体实现方式有很多,结合实际情况选择一种合适的实现方式即可
总结
1、批量逐条更新,这是严令禁止的
效率低下,导致事务时长大大增加,会引发一系列其他的问题
2、数据库的加锁是比较复杂的,不同的数据库的加锁实现也是有区别的
本篇中的死锁案例还是比较好分析的
遇到不好分析的,需要向同事(dba、开发同事等)发出求助,也可以线上求助数据库博主
3、面对不同问题,结合业务来分析出最合适的处理方式
有的业务对性能要求高
有的业务对数据准确性要求高
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |