記一次線上問題 → 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("主線程執行完畢");
}
View Code

   Deadlock 就這麼誕生了!

優化處理

  死鎖產生條件

  死鎖產生的條件,大家還記得嗎?

  回到上訴案例,鎖的持有、申請情況如下

  死鎖自然就產生了

  那麼該如何處理了

  排序處理

  不同線程調用同一個方法處理數據而產生死鎖

  這種情況對處理的數據進行排序處理,使得不同線程申請數據庫鎖的順序保持一致,那麼就不會產生死鎖

  分批處理

  事務時間越短越好

  批量逐條更新,會導致事務持續的時間很長,那麼出現死鎖的概率就越大

  分批處理可以減少事務時長

  加鎖處理

  這裏的鎖指的並非數據庫層面的鎖,而是業務代碼層面的鎖

  可以是 JVM 的鎖,適用於單節點部署的情況

  可以是分佈式鎖,適用於單節點部署,也適用於多節點部署;具體實現方式有很多,結合實際情況選擇一種合適的實現方式即可

總結

  1、批量逐條更新,這是嚴令禁止的

    效率低下,導致事務時長大大增加,會引發一系列其他的問題

  2、數據庫的加鎖是比較複雜的,不同的數據庫的加鎖實現也是有區別的

    本篇中的死鎖案例還是比較好分析的

    遇到不好分析的,需要向同事(dba、開發同事等)發出求助,也可以線上求助數據庫博主

  3、面對不同問題,結合業務來分析出最合適的處理方式

    有的業務對性能要求高

    有的業務對數據準確性要求高

    

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章