Spring 事务失效的常见场景

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 事务失效的常见场景可以归纳为四类:

  1. 自调用 :this.method() 绕过代理,事务注解失效。
  2. 异常类型不对:受检异常默认不回滚,异常被吞掉也不回滚。
  3. 多线程:事务上下文绑定线程,子线程不共享父事务。
  4. 传播行为与隔离级别使用不当 :REQUIRES_NEW 与 NESTED 语义不同,隔离级别影响并发一致性。

掌握这些场景的成因与解决方案,能帮助你在日常开发中快速定位事务问题,写出更健壮的代码。

相关推荐
时间的拾荒人1 小时前
Qt 多线程详解:从 QThread 到实战
开发语言·qt·面试
郑州光合科技余经理1 小时前
海外版外卖加盟:总站与分站配送规则怎么分开管
java·开发语言·前端·后端·uni-app·php·ai编程
2601_962071571 小时前
类变量和全局变量的生命周期有什么区别?
java·开发语言·jvm
code2cat2 小时前
Java进阶篇之AtomicReference:一次替换引用,读到一份完整状态
java·并发编程·不可变对象·atomicreference
专业程序开发源2 小时前
springboot简历管理系统81389-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·spring·php·课程设计
Wang's Blog2 小时前
Java 项目实战: 外卖平台优化-前后端分离开发模式与工程拆分
java·开发语言
geovindu2 小时前
sql: JSON and XML Data Handling in SQL using sql server 2025
大数据·数据库·sqlserver·数据库开发·数据库架构
专业程序开发源2 小时前
springboot社区养老系统44071-计算机课程设计、毕业设计
vue.js·spring boot·后端·python·django·php·课程设计
IT大白鼠2 小时前
图数据库系列 · 第 02 篇——架构拆解:原生图存储到因果集群
数据库·架构·nosql