Spring 事務失效的場景及修復:從原理到實戰
引言
在Java企業級開發中,Spring事務是保障數據一致性的核心機制。但實際項目中,開發者常遇到@Transactional注解加了卻不生效的問題,這本質是對Spring事務實現原理理解不透徹或忽略關鍵細節導致的。
Spring 事務基礎
在分析失效場景前,必須先明確 Spring 事務的核心原理 ——基于 AOP 動態代理實現,這是理解所有失效場景的關鍵。
事務的核心特性(ACID)
- 原子性(Atomicity):事務是不可分割的最小單元,要么全成功,要么全回滾;
- 一致性(Consistency):事務執行前后,數據從一個合法狀態轉換到另一個合法狀態;
- 隔離性(Isolation):多個事務并發執行時,相互不干擾(由隔離級別控制,如READ_COMMITTED);
- 持久性(Durability):事務提交后,數據修改永久保存在數據庫中。
Spring 事務的實現原理
Spring事務通過動態代理為目標Bean生成代理對象,當調用被@Transactional標注的方法時,代理對象會先攔截方法執行:
- 開啟事務(創建數據庫連接,設置事務隔離級別、傳播行為等);
- 調用目標方法(業務邏輯執行);
- 若方法正常返回,提交事務;
- 若方法拋出指定異常,回滾事務;
- 若拋出未指定異常,不回滾(默認僅回滾RuntimeException和Error)。
關鍵結論:只有通過 Spring 容器管理的代理對象調用事務方法,事務才會生效;若繞開代理直接調用(如自調用),事務機制無法觸發。
Spring 事務失效的場景及說明
場景 1:非 public 修飾的方法加 @Transactional
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
// 錯誤:private方法,@Transactional無效
@Transactional
private void createOrder(Order order) {
orderMapper.insert(order);
// 模擬異常
if (order.getAmount() < 0) {
throw new RuntimeException("訂單金額非法");
}
}
}
// 外部調用private方法
public void submitOrder(Order order) {
createOrder(order); // 直接調用,無代理攔截
}Spring事務默認通過AOP動態代理實現,而Spring AOP(無論是JDK動態代理還是CGLIB代理)對方法權限有明確限制:僅攔截public修飾的方法。
場景 2:事務方法內部自調用
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private LogMapper logMapper;
// 事務方法A
@Transactional
public void createOrder(Order order) {
orderMapper.insert(order);
// 錯誤:內部直接調用事務方法B,無代理攔截
this.addOrderLog(order.getId());
}
// 事務方法B(期望單獨事務,但實際失效)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void addOrderLog(Long orderId) {
Log log = new Log();
log.setOrderId(orderId);
log.setContent("訂單創建成功");
logMapper.insert(log);
// 模擬異常:此時addOrderLog的事務不回滾,log仍會插入
throw new RuntimeException("日志記錄異常");
}
}Spring事務的觸發依賴代理對象調用,而當一個事務方法(如methodA)內部直接調用另一個事務方法(如methodB)時,調用過程是目標對象→目標對象,而非代理對象→目標對象,繞開了AOP代理的攔截邏輯,導致methodB的事務不生效。
場景 3:異常被捕獲(try-catch)且未重新拋出
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Transactional
public void createOrder(Order order) {
try {
orderMapper.insert(order);
// 模擬異常
if (order.getAmount() > 10000) {
throw new RuntimeException("訂單金額超過上限");
}
} catch (Exception e) {
// 錯誤:僅打印日志,未重新拋出異常
log.error("創建訂單失敗", e);
}
}
}Spring事務默認僅在方法拋出未被捕獲的RuntimeException或Error時才會觸發回滾。若開發者在事務方法中用try-catch捕獲了異常,且未在catch塊中重新拋出異常,Spring會認為方法執行成功,直接提交事務,導致異常發生時無法回滾。
場景 4:錯誤的事務傳播機制
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
// 錯誤:使用NOT_SUPPORTED,不支持事務
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void createOrder(Order order) {
orderMapper.insert(order);
throw new RuntimeException("模擬異常"); // 異常拋出,但事務未開啟,無法回滾
}
}Spring事務傳播機制定義了多個事務方法嵌套調用時,事務如何傳遞,若選擇了不支持事務或強制不使用事務的傳播行為,會導致事務失效。常見錯誤傳播行為:
- Propagation.NOT_SUPPORTED:以非事務方式執行,若當前存在事務則暫停;
- Propagation.NEVER:以非事務方式執行,若當前存在事務則拋出異常;
- Propagation.SUPPORTS:若當前存在事務則加入,否則以非事務方式執行(非主動開啟事務)。
場景 5:數據源未配置事務管理器
// 錯誤:僅配置數據源,未配置事務管理器
@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl("jdbc:mysql://localhost:3306/order_db");
dataSource.setUsername("root");
dataSource.setPassword("123456");
return dataSource;
}
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}Spring事務的執行依賴事務管理器(TransactionManager),不同的數據源對應不同的事務管理器(如JDBC對應DataSourceTransactionManager,MyBatis對應SqlSessionTransactionManager)。若未在Spring容器中配置事務管理器,@Transactional注解會被忽略,事務無法生效。
Spring Boot中引入spring-boot-starter-jdbc或spring-boot-starter-data-jpa,會自動配置DataSourceTransactionManager,無需手動配置;但自定義數據源時,需手動綁定事務管理器。
場景 6:多線程調用事務方法
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private LogMapper logMapper;
@Transactional
public void createOrder(Order order) {
// 主線程:插入訂單
orderMapper.insert(order);
// 錯誤:子線程調用事務方法,與主線程事務獨立
new Thread(() -> {
addOrderLog(order.getId()); // 子線程事務不生效(或與主線程獨立)
}).start();
// 模擬主線程異常:主線程回滾(訂單不插入),但子線程日志已插入
throw new RuntimeException("主線程異常");
}
@Transactional
public void addOrderLog(Long orderId) {
Log log = new Log();
log.setOrderId(orderId);
logMapper.insert(log);
}
}Spring事務是線程綁定的,事務上下文(如數據庫連接)存儲在ThreadLocal中。當主線程調用子線程執行事務方法時,子線程無法繼承主線程的事務上下文,導致子線程的事務與主線程獨立,若子線程拋出異常,主線程事務不會回滾(反之亦然),可能出現數據一致性問題。
































