1. 引言
Spring 事务管理是 Java 后端开发中最常用的能力之一,但也是踩坑重灾区。很多同学在开发中遇到「明明加了 @Transactional,数据却没回滚」的问题,排查半天才发现是事务根本没生效。本文梳理 Spring 事务失效的常见场景,从自调用、异常类型、多线程、传播行为与隔离级别等角度逐一剖析,并给出可运行的实战示例。
2. 自调用导致事务失效
2.1 问题现象
java
@Service
public class OrderService {
@Transactional
public void createOrder(Order order) {
// 业务逻辑
updateStock(order.getProductId());
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateStock(Long productId) {
// 扣减库存
}
}
当外部调用 createOrder 时,updateStock 上的 REQUIRES_NEW 并不会生效。原因在于:createOrder 内部通过 this.updateStock() 调用,走的是 this 引用,而非 Spring 代理对象,因此事务注解被绕过。
2.2 原理分析
Spring 事务基于 AOP 代理实现。只有通过代理对象调用方法时,@Transactional 才会被拦截并织入事务逻辑。自调用(this.method())直接调用目标对象自身的方法,不经过代理,事务注解自然失效。
下面是自调用导致事务失效的调用关系图:
#mermaid-svg-WxKazcKbcz0NLpq8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-WxKazcKbcz0NLpq8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-WxKazcKbcz0NLpq8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-WxKazcKbcz0NLpq8 .error-icon{fill:#552222;}#mermaid-svg-WxKazcKbcz0NLpq8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-WxKazcKbcz0NLpq8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-WxKazcKbcz0NLpq8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-WxKazcKbcz0NLpq8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-WxKazcKbcz0NLpq8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-WxKazcKbcz0NLpq8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-WxKazcKbcz0NLpq8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-WxKazcKbcz0NLpq8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-WxKazcKbcz0NLpq8 .marker.cross{stroke:#333333;}#mermaid-svg-WxKazcKbcz0NLpq8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-WxKazcKbcz0NLpq8 p{margin:0;}#mermaid-svg-WxKazcKbcz0NLpq8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-WxKazcKbcz0NLpq8 .cluster-label text{fill:#333;}#mermaid-svg-WxKazcKbcz0NLpq8 .cluster-label span{color:#333;}#mermaid-svg-WxKazcKbcz0NLpq8 .cluster-label span p{background-color:transparent;}#mermaid-svg-WxKazcKbcz0NLpq8 .label text,#mermaid-svg-WxKazcKbcz0NLpq8 span{fill:#333;color:#333;}#mermaid-svg-WxKazcKbcz0NLpq8 .node rect,#mermaid-svg-WxKazcKbcz0NLpq8 .node circle,#mermaid-svg-WxKazcKbcz0NLpq8 .node ellipse,#mermaid-svg-WxKazcKbcz0NLpq8 .node polygon,#mermaid-svg-WxKazcKbcz0NLpq8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-WxKazcKbcz0NLpq8 .rough-node .label text,#mermaid-svg-WxKazcKbcz0NLpq8 .node .label text,#mermaid-svg-WxKazcKbcz0NLpq8 .image-shape .label,#mermaid-svg-WxKazcKbcz0NLpq8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-WxKazcKbcz0NLpq8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-WxKazcKbcz0NLpq8 .rough-node .label,#mermaid-svg-WxKazcKbcz0NLpq8 .node .label,#mermaid-svg-WxKazcKbcz0NLpq8 .image-shape .label,#mermaid-svg-WxKazcKbcz0NLpq8 .icon-shape .label{text-align:center;}#mermaid-svg-WxKazcKbcz0NLpq8 .node.clickable{cursor:pointer;}#mermaid-svg-WxKazcKbcz0NLpq8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-WxKazcKbcz0NLpq8 .arrowheadPath{fill:#333333;}#mermaid-svg-WxKazcKbcz0NLpq8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-WxKazcKbcz0NLpq8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-WxKazcKbcz0NLpq8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-WxKazcKbcz0NLpq8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-WxKazcKbcz0NLpq8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-WxKazcKbcz0NLpq8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-WxKazcKbcz0NLpq8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-WxKazcKbcz0NLpq8 .cluster text{fill:#333;}#mermaid-svg-WxKazcKbcz0NLpq8 .cluster span{color:#333;}#mermaid-svg-WxKazcKbcz0NLpq8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-WxKazcKbcz0NLpq8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-WxKazcKbcz0NLpq8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-WxKazcKbcz0NLpq8 .icon-shape,#mermaid-svg-WxKazcKbcz0NLpq8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-WxKazcKbcz0NLpq8 .icon-shape p,#mermaid-svg-WxKazcKbcz0NLpq8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-WxKazcKbcz0NLpq8 .icon-shape .label rect,#mermaid-svg-WxKazcKbcz0NLpq8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-WxKazcKbcz0NLpq8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-WxKazcKbcz0NLpq8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-WxKazcKbcz0NLpq8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 通过代理调用
拦截 @Transactional
this.updateStock() 自调用
绕过代理,事务注解失效
正常代理调用
REQUIRES_NEW 生效
REQUIRES_NEW 不生效
外部调用方
Spring 代理对象
目标方法 createOrder()
目标方法 updateStock()
目标方法 updateStock()
独立事务
无独立事务
图中实线表示正常经过代理的调用路径,虚线表示自调用绕过代理的路径------createOrder 内部通过 this.updateStock() 直接调用目标对象自身的方法,没有经过 Spring 代理对象,因此 REQUIRES_NEW 不会生效。
2.3 解决方案
方案一:注入自身代理
java
@Service
public class OrderService {
@Autowired
private OrderService self;
@Transactional
public void createOrder(Order order) {
// 业务逻辑
self.updateStock(order.getProductId());
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateStock(Long productId) {
// 扣减库存
}
}
方案二:拆分到不同 Bean
java
@Service
public class OrderService {
@Autowired
private StockService stockService;
@Transactional
public void createOrder(Order order) {
// 业务逻辑
stockService.updateStock(order.getProductId());
}
}
方案三:使用 TransactionTemplate 编程式事务
java
@Service
public class OrderService {
@Autowired
private TransactionTemplate transactionTemplate;
public void createOrder(Order order) {
transactionTemplate.execute(status -> {
// 业务逻辑
updateStock(order.getProductId());
return null;
});
}
}
3. 异常类型不对导致事务不回滚
3.1 问题现象
java
@Transactional
public void createOrder(Order order) {
try {
// 业务逻辑
throw new RuntimeException("库存不足");
} catch (Exception e) {
// 捕获异常后未抛出
log.error("下单失败", e);
}
}
事务方法内部捕获异常后没有重新抛出,Spring 认为方法正常返回,于是提交事务,数据没有回滚。
3.2 原理分析
Spring 默认只对 RuntimeException 和 Error 回滚,对受检异常(Exception 的子类)默认不回滚。同时,异常必须从 @Transactional 方法中抛出,才能触发回滚逻辑。
3.3 解决方案
方案一:让异常抛出方法
java
@Transactional
public void createOrder(Order order) {
// 业务逻辑
throw new RuntimeException("库存不足");
}
方案二:指定回滚异常类型
java
@Transactional(rollbackFor = Exception.class)
public void createOrder(Order order) throws Exception {
// 业务逻辑
throw new Exception("库存不足");
}
方案三:手动回滚
java
@Transactional
public void createOrder(Order order) {
try {
// 业务逻辑
throw new RuntimeException("库存不足");
} catch (Exception e) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
log.error("下单失败", e);
}
}
4. 多线程环境下事务不传播
4.1 问题现象
java
@Transactional
public void batchProcess(List<Order> orders) {
orders.parallelStream().forEach(order -> {
// 每个子线程中的数据库操作不在同一事务中
updateOrder(order);
});
}
4.2 原理分析
Spring 事务默认绑定在当前线程的 ThreadLocal 中。新开的子线程无法继承父线程的事务上下文,因此子线程中的数据库操作各自独立提交,不参与父事务。
4.3 解决方案
方案一:避免在事务中开多线程
将多线程处理放在事务方法之外,或使用 TransactionTemplate 在子线程中手动开启事务。
方案二:子线程中使用编程式事务
java
public void batchProcess(List<Order> orders) {
orders.parallelStream().forEach(order -> {
transactionTemplate.execute(status -> {
updateOrder(order);
return null;
});
});
}
方案三:使用 TransactionSynchronizationManager 手动传递事务上下文(不推荐,复杂且易出错)
5. 传播行为与隔离级别实战
5.1 传播行为速查
| 传播行为 | 说明 |
|---|---|
REQUIRED(默认) |
当前有事务则加入,没有则新建 |
REQUIRES_NEW |
无论当前有无事务,都新建一个独立事务 |
SUPPORTS |
当前有事务则加入,没有则以非事务方式执行 |
NOT_SUPPORTED |
以非事务方式执行,挂起当前事务 |
MANDATORY |
当前必须有事务,否则抛异常 |
NEVER |
当前必须没有事务,否则抛异常 |
NESTED |
嵌套事务,基于 Savepoint 实现 |
5.2 隔离级别速查
| 隔离级别 | 说明 |
|---|---|
DEFAULT |
使用数据库默认隔离级别 |
READ_UNCOMMITTED |
读未提交,存在脏读 |
READ_COMMITTED |
读已提交,解决脏读 |
REPEATABLE_READ |
可重复读,解决不可重复读 |
SERIALIZABLE |
串行化,解决幻读,性能最低 |
5.3 实战:REQUIRES_NEW 与 REQUIRED 的区别
java
@Service
public class OrderService {
@Autowired
private OrderDao orderDao;
@Autowired
private LogService logService;
@Transactional
public void createOrder(Order order) {
orderDao.insert(order);
// 即使这里抛出异常,logService 的日志记录已独立提交
logService.writeLog("创建订单:" + order.getId());
throw new RuntimeException("业务异常");
}
}
@Service
public class LogService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void writeLog(String message) {
// 独立事务,不受外层回滚影响
}
}
5.4 实战:NESTED 与 REQUIRES_NEW 的选择
java
@Transactional
public void createOrder(Order order) {
try {
orderDao.insert(order);
// NESTED:外层回滚时,这里也会回滚(Savepoint 回滚)
// REQUIRES_NEW:外层回滚时,这里不会回滚
couponService.deductCoupon(order.getCouponId());
} catch (Exception e) {
// 业务降级处理
}
}
选择建议:
- 需要「部分回滚」且参与外层事务的最终一致性 → 使用
NESTED。 - 需要「独立提交」且不受外层事务影响(如审计日志) → 使用
REQUIRES_NEW。
6. 事务失效排查清单
遇到事务不生效的问题时,可以按下面的清单逐项排查,快速定位根因:
| 排查项 | 快速验证方法 | 常见误区 |
|---|---|---|
1. 方法是否为 public |
检查方法修饰符,@Transactional 只对 public 方法生效 |
误以为 private/protected 方法也能被代理拦截 |
| 2. 是否通过代理调用 | 确认调用方注入的是 Bean 而非 this,自调用会绕过代理 |
在同类内部用 this.method() 调用,事务注解失效 |
| 3. 异常是否被吞掉 | 检查方法内是否有 try-catch 且未重新抛出 |
捕获异常后只记日志不抛出,Spring 认为方法正常返回 |
4. 异常类型是否为 RuntimeException |
确认抛出的异常是 RuntimeException 或 Error,或已配置 rollbackFor |
受检异常默认不回滚,需显式指定 rollbackFor = Exception.class |
| 5. 是否在多线程中调用 | 检查是否使用 parallelStream、@Async 或手动 new Thread |
子线程不共享父线程的 ThreadLocal 事务上下文 |
| 6. 事务管理器是否配置正确 | 确认 DataSourceTransactionManager 已注入且数据源正确 |
多个数据源时未指定 transactionManager,导致事务管理错乱 |
6. 总结
Spring 事务失效的常见场景可以归纳为四类:
- 自调用 :
this.method()绕过代理,事务注解失效。 - 异常类型不对:受检异常默认不回滚,异常被吞掉也不回滚。
- 多线程:事务上下文绑定线程,子线程不共享父事务。
- 传播行为与隔离级别使用不当 :
REQUIRES_NEW与NESTED语义不同,隔离级别影响并发一致性。
掌握这些场景的成因与解决方案,能帮助你在日常开发中快速定位事务问题,写出更健壮的代码。