一、Spring 事務的底層原理
1. 核心機制
- 動態代理(AOP):
Spring 通過動態代理(JDK 或 CGLIB)生成代理對象,攔截被@Transactional
注解標記的方法。 - 事務攔截器:
TransactionInterceptor
負責管理事務的生命周期(開啟、提交、回滾)。 - 事務管理器:
PlatformTransactionManager
實現類(如DataSourceTransactionManager
)負責底層事務操作(如 JDBC 的commit()
)。 - 線程綁定:
通過ThreadLocal
(TransactionSynchronizationManager
)存儲當前事務的數據庫連接,確保同一線程內多個操作共享同一事務。
2. 關鍵流程
// 偽代碼:事務攔截器邏輯
public Object invoke(MethodInvocation invocation) {
// 1. 獲取事務屬性(@Transactional配置)
TransactionAttribute txAttr = getTransactionAttribute(invocation.getMethod());
// 2. 獲取事務管理器
PlatformTransactionManager tm = determineTransactionManager(txAttr);// 3. 開啟事務(根據傳播行為決定是否新建事務)
TransactionStatus status = tm.getTransaction(txAttr);try {// 4. 執行目標方法Object result = invocation.proceed();// 5. 提交事務tm.commit(status);return result;
} catch (Exception ex) {// 6. 回滾事務(根據rollbackFor規則)completeTransactionAfterThrowing(txAttr, status, ex);throw ex;
}
}
二、常見陷阱及代碼示例
陷阱 1:自調用導致事務失效
問題:同類內部方法調用(未經過代理對象),事務注解失效。
@Service
public class UserService {
public void createUser() {
// 直接調用內部方法,事務不生效!
this.insertUser();
}
@Transactional
public void insertUser() {// 插入用戶到數據庫
}
}
原因:this.insertUser()
是目標對象直接調用,未經過代理對象,事務攔截器未被觸發。
解決:
- 方法 1:注入自身代理對象(通過
AopContext
):
@EnableAspectJAutoProxy(exposeProxy = true) // 啟動類開啟暴露代理
public class UserService {
public void createUser() {
UserService proxy = (UserService) AopContext.currentProxy();
proxy.insertUser(); // 通過代理對象調用
}
}
- 方法 2:拆分類,將
insertUser
放到另一個 Bean 中。
陷阱 2:異常被捕獲未拋出
問題:事務方法中捕獲異常但未重新拋出,導致事務無法回滾。
@Transactional
public void updateUser() {
try {
userDao.update(user); // 可能拋出SQLException
} catch (SQLException e) {
// 捕獲異常但未拋出,事務不會回滾!
log.error(“更新失敗”, e);
}
}
原因:Spring 默認只對 RuntimeException
和 Error
回滾,且必須拋出異常。
解決:
- 方法 1:拋出
RuntimeException
:
catch (SQLException e) {
throw new RuntimeException(“更新失敗”, e); // 觸發回滾
}
- 方法 2:配置
@Transactional(rollbackFor = SQLException.class)
。
陷阱 3:事務傳播行為誤解
問題:嵌套事務未按預期回滾。
@Transactional
public void outerMethod() {
userDao.insertUser();
try {
innerService.innerMethod();
} catch (Exception e) {
// 期望 innerMethod 回滾,但 outerMethod 繼續提交
}
}
@Service
public class InnerService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void innerMethod() {
userDao.updateUser(); // 拋出異常
}
}
現象:如果 innerMethod
拋出異常,innerMethod
的事務會回滾,但 outerMethod
的事務仍會提交(因為 innerMethod
的事務是獨立的)。
解決:
- 如果希望
outerMethod
在innerMethod
失敗時整體回滾,需在outerMethod
中不捕獲異常,或重新拋出異常。
陷阱 4:數據庫引擎不支持事務
問題:使用 MyISAM 引擎的 MySQL 表不支持事務。
CREATE TABLE user (
id INT PRIMARY KEY
) ENGINE=MyISAM; – 不支持事務
現象:即使代碼正確配置事務,操作仍不會回滾。
解決:使用 InnoDB 引擎:
CREATE TABLE user (…) ENGINE=InnoDB;
陷阱 5:非 public 方法事務失效
問題:@Transactional
標記在非 public 方法上,事務不生效。
@Service
public class UserService {
@Transactional
private void internalUpdate() { // 非 public 方法!
userDao.update(user);
}
}
原因:Spring 默認通過代理實現 AOP,無法攔截 private/protected 方法。
解決:
- 將方法改為
public
。 - 使用 AspectJ 模式(配置
@EnableTransactionManagement(mode = AdviceMode.ASPECTJ)
)。
陷阱 6:多線程下事務上下文丟失
問題:新線程無法繼承原線程的事務上下文。
@Transactional
public void process() {
new Thread(() -> {
userDao.updateUser(); // 新線程無法共享事務
}).start();
}
原因:TransactionSynchronizationManager
使用 ThreadLocal
,不同線程無法共享事務資源。
解決:
- 避免在事務方法中啟動新線程操作數據庫。
- 使用編程式事務管理(手動控制事務邊界)。
三、總結
關鍵點
- 動態代理 + 事務管理器 + ThreadLocal 是 Spring 事務的核心。
- 自調用、異常處理、傳播行為、數據庫支持 是常見陷阱。
- 通過代碼審查、日志(如
AbstractPlatformTransactionManager
的DEBUG
日志)排查問題。
最佳實踐
- 使用
@Transactional
時明確指定rollbackFor
。 - 避免同類自調用(通過代理對象或拆分類)。
- 確保數據庫引擎支持事務(如 InnoDB)。
- 事務方法保持
public
修飾符。