一次 Spring 事务传播机制源码走读:从误用 @Transactional 到理解嵌套事务的边界

"@Transactional 不是套个注解就万事大吉的!"

会议室里,小李指着白板上的一段代码,语气激动:"我们这个订单服务里,外层方法加了 @Transactional,内层又调了一个带 REQUIRES_NEW 的子方法,结果事务没回滚,数据不一致了!"

老王皱眉:"你是不是没看 Spring 的事务传播机制?REQUIRES_NEW 会挂起当前事务,新建一个,但如果你外层没正确处理异常,内层提交后外层回滚,那不就脏数据了?"

"我查了文档,说 REQUIRES_NEW 会独立提交啊......"

"文档没错,但你没看源码,不知道'挂起'到底是怎么实现的。"我敲了敲桌子,"今天咱们不靠猜,直接从 Spring 源码走一遍,看看 @Transactional 在嵌套调用时,事务上下文是怎么流转的。"


需求约束:订单履约中的事务一致性挑战

我们的业务场景是典型的订单履约系统:用户下单后,系统需要完成库存扣减、积分扣除、优惠券核销、物流创建等一系列操作。这些操作必须保证原子性------要么全成功,要么全回滚。

初期方案很简单:在 OrderService.createOrder() 方法上加 @Transactional,内部依次调用 inventoryService.deduct()pointsService.deduct()couponService.use()

但随着业务复杂化,出现了新的需求:

  • 积分扣除需要独立审计日志,即使主订单失败,积分操作也要记录(用于对账);
  • 优惠券核销必须立即生效,不能因其他服务失败而回滚;
  • 物流创建是异步的,但必须保证最终一致性。

于是团队引入了 Propagation.REQUIRES_NEW,试图让某些子操作"独立提交"。但上线后却频繁出现数据不一致:积分扣了、订单没生成;优惠券用了、库存没减。

问题出在哪?我们决定从源码层面拆解 Spring 的事务管理机制。


架构设计:Spring 事务管理的核心组件

Spring 的事务管理基于 AOP 实现,核心组件包括:

  • TransactionInterceptor:事务切面,负责在方法前后开启/提交/回滚事务;
  • PlatformTransactionManager:事务管理器接口,如 DataSourceTransactionManager
  • TransactionStatus:事务状态对象,记录当前事务是否为新事务、是否已完成等;
  • TransactionSynchronizationManager:线程本地存储(ThreadLocal)管理事务资源(如 Connection)、同步器等。

关键点在于:事务的"挂起"与"恢复"是通过 ThreadLocal 实现的


关键代码/组件:从误用到正确的演进

错误方案:盲目使用 REQUIRES_NEW
java 复制代码
@Service
public class OrderService {

    @Autowired
    private InventoryService inventoryService;

    @Autowired
    private PointsService pointsService;

    @Transactional
    public void createOrder(Order order) {
        inventoryService.deduct(order.getSkuId(), order.getQuantity());
        pointsService.deduct(order.getUserId(), order.getPoints()); // 使用 REQUIRES_NEW
        // 其他操作...
    }
}

@Service
public class PointsService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void deduct(Long userId, Integer points) {
        // 扣除积分并记录审计日志
        pointsRepository.deduct(userId, points);
        auditLogRepository.log(userId, "DEDUCT", points);
    }
}

表面看逻辑清晰,但问题在于:外层事务异常时,内层已提交,无法回滚

更隐蔽的问题是:如果外层事务在 pointsService.deduct() 执行期间被挂起,但后续代码抛出异常,外层事务回滚,而内层事务已提交,导致数据不一致。

源码走读:TransactionInterceptor 如何处理传播行为?

我们进入 TransactionInterceptor.invoke() 方法,核心逻辑如下:

java 复制代码
protected Object invokeWithinTransaction(Method method, Class<?> targetClass, final InvocationCallback invocation) {
    TransactionAttributeSource tas = getTransactionAttributeSource();
    final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
    final PlatformTransactionManager tm = determineTransactionManager(txAttr);
    final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);

    if (txAttr != null && tm instanceof CallbackPreferringPlatformTransactionManager) {
        // 省略回调逻辑
    } else {
        TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
        Object retVal = null;
        try {
            retVal = invocation.proceedWithInvocation();
        } catch (Throwable ex) {
            completeTransactionAfterThrowing(txInfo, ex);
            throw ex;
        } finally {
            cleanupTransactionInfo(txInfo);
        }
        commitTransactionAfterReturning(txInfo);
        return retVal;
    }
}

重点在 createTransactionIfNecessary() 方法。我们深入查看其对 REQUIRES_NEW 的处理:

java 复制代码
protected TransactionInfo createTransactionIfNecessary(PlatformTransactionManager tm, TransactionAttribute txAttr, final String joinpointIdentification) {
    if (txAttr != null && txAttr.getName() == null) {
        txAttr = new DelegatingTransactionAttribute(txAttr) {
            @Override
            public String getName() {
                return joinpointIdentification;
            }
        };
    }

    TransactionStatus status = null;
    if (txAttr != null) {
        if (txAttr.isReadOnly()) {
            status = tm.getTransaction(txAttr);
        } else {
            status = tm.getTransaction(txAttr);
        }
    }
    return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status);
}

继续进入 AbstractPlatformTransactionManager.getTransaction()

java 复制代码
public final TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException {
    Object transaction = doGetTransaction();

    if (isExistingTransaction(transaction) && definition.getPropagationBehavior() != TransactionDefinition.PROPAGATION_REQUIRES_NEW) {
        // 当前有事务,且不是 REQUIRES_NEW,则加入现有事务
        return handleExistingTransaction(definition, transaction, debugEnabled);
    }

    // 当前无事务,或 propagation = REQUIRES_NEW
    SuspendedResourcesHolder suspendedResources = suspend(null);
    boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
    DefaultTransactionStatus status = newTransactionStatus(definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
    doBegin(transaction, definition);
    prepareSynchronization(status, definition);
    return status;
}

关键来了:当传播行为为 REQUIRES_NEW 时,Spring 会调用 suspend(null) 挂起当前事务

suspend() 方法源码:

java 复制代码
protected final SuspendedResourcesHolder suspend(Object transaction) throws TransactionException {
    if (currentTransaction != null) {
        // 挂起当前事务资源(Connection、同步器等)
        Object resumed = doSuspend(transaction);
        // 清除 ThreadLocal 中的事务上下文
        TransactionSynchronizationManager.setSynchronizationOnCurrentThread(false);
        TransactionSynchronizationManager.bindResource(getDataSource(), null);
        return new SuspendedResourcesHolder(resumed);
    }
    return null;
}

这意味着:内层方法执行时,外层事务的 Connection 被释放,ThreadLocal 被清空。内层开启全新事务,使用新的 Connection。

当内层方法执行完毕,commitTransactionAfterReturning() 会提交该事务。

但外层方法继续执行时,若抛出异常,外层事务回滚,而内层已提交,无法撤销。

正确方案:明确事务边界与异常处理

我们重新设计:

  1. 避免在业务关键路径中使用 REQUIRES_NEW,除非明确接受"部分提交";
  2. 将必须独立提交的操作异步化,通过消息队列保证最终一致性;
  3. 使用编程式事务控制更细粒度

改造后代码:

java 复制代码
@Service
public class OrderService {

    @Autowired
    private InventoryService inventoryService;

    @Autowired
    private PointsService pointsService;

    @Autowired
    private TransactionTemplate transactionTemplate;

    @Autowired
    private AuditLogProducer auditLogProducer;

    @Transactional
    public void createOrder(Order order) {
        inventoryService.deduct(order.getSkuId(), order.getQuantity());
        
        // 使用编程式事务,确保积分操作独立提交
        transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
        transactionTemplate.execute(status -> {
            pointsService.deduct(order.getUserId(), order.getPoints());
            return null;
        });

        // 异步发送审计日志,不阻塞主事务
        auditLogProducer.sendAuditLog(order.getUserId(), "ORDER_CREATE", order.getId());
    }
}

同时,PointsService.deduct() 不再加 @Transactional,避免双重代理问题。


复盘:从源码理解事务传播的本质

这次走读让我们意识到:

  • @Transactional 不是银弹,传播行为的选择必须基于业务语义;
  • REQUIRES_NEW 的真正代价是"事务隔离",而非"性能开销";
  • ThreadLocal 是 Spring 事务上下文的核心载体,理解其生命周期至关重要;
  • 编程式事务在复杂场景下比声明式更可控。

我们也发现一个常见误区:很多人以为 @Transactional 是"数据库事务"的代理,其实它是"Spring 事务同步机制"的封装。真正的数据库事务由 Connection 控制,而 Spring 通过 ThreadLocal 绑定 Connection,实现声明式管理。


技术补丁包

  1. Spring 事务传播机制核心原理 原理:基于 AOP 拦截方法调用,通过 TransactionSynchronizationManager 使用 ThreadLocal 绑定事务资源(如 Connection),不同传播行为决定是加入现有事务还是新建/挂起事务。 设计动机:提供声明式事务管理,避免手动管理 Connection 和事务边界。 边界条件:REQUIRES_NEW 会挂起当前事务,若外层异常回滚,内层已提交无法撤销。 落地建议:优先使用 REQUIRED,仅在明确需要独立提交时使用 REQUIRES_NEW,并配合异步补偿机制。

  2. TransactionSynchronizationManager 的 ThreadLocal 机制 原理:使用 ThreadLocal<Map<Object, Object>> 存储当前线程的事务资源(dataSource -> Connection)、同步器列表、事务名称等。 设计动机:确保同一线程内多个事务拦截器能共享事务上下文,避免重复开启事务。 边界条件:异步线程、线程池复用会导致 ThreadLocal 污染或丢失,需手动清理或传递上下文。 落地建议:在异步场景中,使用 TransactionContextHolder 或消息头传递事务 ID,避免依赖 ThreadLocal。

  3. REQUIRES_NEW 与数据库连接的绑定关系 原理:每次 REQUIRES_NEW 都会调用 DataSourceUtils.getConnection() 获取新连接,执行 setAutoCommit(false) 开启新事务。 设计动机:确保事务隔离性,避免脏读、不可重复读。 边界条件:频繁使用 REQUIRES_NEW 会导致连接池压力增大,增加数据库负载。 落地建议:结合连接池监控(如 HikariCP 的 active connections),限制 REQUIRES_NEW 使用频率,或改用异步消息解耦。

  4. 声明式事务 vs 编程式事务的选型建议 原理:声明式基于 @Transactional + AOP,编程式基于 TransactionTemplateTransactionManager 手动控制。 设计动机:声明式简洁,编程式灵活,适用于复杂事务边界控制。 边界条件:声明式无法在同一个类内部调用时生效(AOP 代理限制),编程式可精确控制事务范围。 落地建议:简单场景用声明式,嵌套事务、条件回滚、多数据源等复杂场景优先使用编程式事务。

相关推荐
Java面试题总结2 小时前
Spring - Bean 生命周期
java·spring·rpc
二月夜5 小时前
Spring循环依赖深度解析:从三级缓存原理到跨环境“灵异”现象
java·spring
九皇叔叔8 小时前
003-SpringSecurity-Demo 统一响应类
java·javascript·spring·springsecurity
计算机学姐10 小时前
基于SpringBoot的兴趣家教平台系统
java·spring boot·后端·spring·信息可视化·tomcat·intellij-idea
總鑽風10 小时前
单点登录springcloud+mysql
后端·spring·spring cloud
却话巴山夜雨时i10 小时前
互联网大厂Java面试场景:从基础到微服务的循序渐进提问
java·数据库·spring·微服务·面试·消息队列·技术栈
zhangzhi197981559210 小时前
Spring AI A2A 协议实战:构建跨平台的 AI Agent 协作系统
java·人工智能·spring
弹简特12 小时前
【JavaEE31-后端部分】Spring事务入门:从编程式到@Transactional,带你轻松搞定数据一致性
java·spring·spring事务
cheoyeon13 小时前
ruoyi-cloud项目开发
spring·spring cloud·maven