Spring 事務雖然用起來簡單(加個 @Transactional 就走人),但其底層機制若不理解,極易踩坑。本文通過兩個實戰案例,深度剖析「唯讀事務卻能寫入」與「異常捕獲導致事務不回滾」的根因。
@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 未執行 → 數據部分寫入@Transactional(propagation = Propagation.REQUIRES_NEW)
public Integer getUID2(String key, String type) { ... }
開啟新事務,內層異常觸發回滾,外層捕獲異常不影響內層事務狀態。對比圖解:
@Transactional 不止 readOnly,propagation、isolation、rollbackFor 同樣關鍵