首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 綜合教學

Spring 事務兩大經典坑:readOnly 失效與傳播行為導致的回滾異常

     編輯:Java 綜合教學

Spring 事務兩大經典坑:readOnly 失效與傳播行為導致的回滾異常

Spring 事務雖然用起來簡單(加個 @Transactional 就走人),但其底層機制若不理解,極易踩坑。本文通過兩個實戰案例,深度剖析「唯讀事務卻能寫入」與「異常捕獲導致事務不回滾」的根因。

問題一:@Transactional(readOnly = true) 卻能正常寫入數據?

現象重現

@Transactional(readOnly = true)
public Integer getUID(String key, String type) {
    keyGeneratorDao.insert(key, type);  // 寫操作
    keyGeneratorDao.update(key, type);  // 寫操作
    return keyGeneratorDao.select(key, type);
}

理論上應拋異常 Connection is read-only,但線上卻運行正常。為什麼?

層層遞進的排查過程

坑 1:同類內部調用事務方法失效(CGLIB 代理特性)
若事務方法與調用者在同一類中,直接調用繞過代理,事務不生效。解決:將事務方法提取到獨立 Service 類。

坑 2:測試代碼與生產代碼不一致
初始測試將 readOnly 加在純查詢方法上,而非包含寫操作的方法。修正後確認:直接在含寫操作的方法上加 readOnly=true 確會拋異常。

坑 3:非事務方法調用本類事務方法,事務同樣失效!
這是線上「正常運行」的真相:

// 非事務方法
public String genOrderCode(Date orderDate) {
    // ... 
    Integer uid = getUID(ticketDate, ORDER_CODE); // 內部調用有 @Transactional(readOnly=true) 的方法
    return ticketDate + genRandom2(uid.toString(), 3, 3);
}

結論:外部調用非事務方法 genOrderCode → 內部調用事務方法 getUID → 事務失效 → readOnly 不生效 → 寫入成功。這是典型的「自調用繞過代理」問題。


問題二:捕獲異常後事務不回滾,數據不一致

場景代碼

@Transactional
public String genOrderCode(Date orderDate) {
    try {
        Integer uid = getUID2(ticketDate, ORDER_CODE); // 內部拋異常
        return ticketDate + genRandom2(uid.toString(), 3, 3);
    } catch (Exception ex) {
        System.out.println(ex); // 吞掉異常!
    }
    return null;
}

@Transactional
public Integer getUID2(String key, String type) {
    keyGeneratorDao.insert(key, type);
    Integer.parseInt("aaa"); // 強制拋異常
    keyGeneratorDao.update(key, type); // 不會執行
    return keyGeneratorDao.select(key, type);
}

現象

測試顯示「提交成功」,但數據庫只有 insert 無 update —— 事務未回滾,數據不一致。

根因分析

  • 默認傳播行為 PROPAGATION_REQUIRED:雙方同屬一個事務
  • genOrderCode 捕獲異常並正常返回 → Spring 認定事務提交無誤 → 事務提交
  • 但 getUID2 異常導致 update 未執行 → 數據部分寫入

解決方案:PROPAGATION_REQUIRES_NEW

@Transactional(propagation = Propagation.REQUIRES_NEW)
public Integer getUID2(String key, String type) { ... }

開啟新事務,內層異常觸發回滾,外層捕獲異常不影響內層事務狀態。對比圖解:

  • REQUIRED:同一事務,外層吞異常導致整體提交
  • REQUIRES_NEW:獨立事務,內層回滾不影響外層邏輯

核心經驗總結

  1. 上下文分析:單看代碼片段易誤判,必須結合調用鏈路(Controller → Service → Dao)
  2. 測試代碼要「像」:單元測試必須復現生產環境的調用結構(含非事務方法包裹)
  3. 全面掌握註解屬性:@Transactional 不止 readOnly,propagation、isolation、rollbackFor 同樣關鍵
  4. 避免自調用:事務方法互調請注入自身 Bean 或拆分類,否則繞過 AOP 代理
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved