• 记一次线上问题 → Deadlock 的分析与优化


    开心一刻

      今天女朋友很生气

      女朋友:我发现你们男的,都挺单纯的

      我:这话怎么说

      女朋友:脑袋里就只想三件事,搞钱,跟谁喝点,还有这娘们真好看

      我:你错了,其实我们男人吧,每天只合计一件事

      女朋友:啥事呀?

      我:这娘们真好看,得搞钱跟她喝点

    问题复现

      需求背景

       MySQL8.0.30 ,隔离级别是默认的,也就是 REPEATABLE-READ 

      表: tbl_class_student ,id 非自增,整张表的全部字段数据都是从上游服务进行同步

      需求:上游服务发送同步MQ,本服务收到消息后再调上游服务接口,查询全量数据,对 tbl_class_student 表数据进行更新,若记录存在则更新,不存在则插入

      这需求是不是很明确?放心,没有下套!

      线上问题

      通过线上异常日志,最终定位到如下代码

      咋一看,这代码是不是无比的清晰明了?

      都不用注释,就能清楚的知道这个代码是在做什么:逐行更新,存在则更新,不存在则插入

      是不是无比的契合需求?

      但是,真的就完美无瑕吗

      且看我表演一波

      表演代码如下:

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void batchSaveOrUpdate(List 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 classStudents1 = new ArrayList<>();
        classStudents1.add(classStudent);
        classStudents1.add(classStudent1);
    
        List 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、面对不同问题,结合业务来分析出最合适的处理方式

        有的业务对性能要求高

        有的业务对数据准确性要求高

        

  • 相关阅读:
    UNIX网络编程卷一 学习笔记 第二章 传输层:TCP、UDP和SCTP
    Java天花板从了解java&&JDK到底是什么开始?
    【TensorFlow】环境配置-----TensorFlow 安装与配置
    esp32 arduino使用多个串口时如何查看serial1,serial2所对应的引脚定义
    笔试强训(三十七)
    国产集成开发环境工具 CEC-IDE
    示例:WPF中TreeView自定义TreeNode泛型绑定对象来实现级联勾选
    艺人百度百科怎么创建
    IEnumerable与IQueryable延迟加载
    Redis 服务集群、哨兵、缓存及持久化的实现原理和应用场景
  • 原文地址:https://www.cnblogs.com/youzhibing/p/17578206.html