開心一刻 今天女朋友很生氣 女朋友:我發現你們男的,都挺單純的 我:這話怎麼說 女朋友:腦袋裡就只想三件事,搞錢,跟誰喝點,還有這娘們真好看 我:你錯了,其實我們男人吧,每天只合計一件事 女朋友:啥事呀? 我:這娘們真好看,得搞錢跟她喝點 問題復現 需求背景 MySQL8.0.30 ,隔離級別是預設 ...
開心一刻
今天女朋友很生氣
女朋友:我發現你們男的,都挺單純的
我:這話怎麼說
女朋友:腦袋裡就只想三件事,搞錢,跟誰喝點,還有這娘們真好看
我:你錯了,其實我們男人吧,每天只合計一件事
女朋友:啥事呀?
我:這娘們真好看,得搞錢跟她喝點
問題復現
需求背景
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、面對不同問題,結合業務來分析出最合適的處理方式
有的業務對性能要求高
有的業務對數據準確性要求高