"@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() 会提交该事务。
但外层方法继续执行时,若抛出异常,外层事务回滚,而内层已提交,无法撤销。
正确方案:明确事务边界与异常处理
我们重新设计:
- 避免在业务关键路径中使用 REQUIRES_NEW,除非明确接受"部分提交";
- 将必须独立提交的操作异步化,通过消息队列保证最终一致性;
- 使用编程式事务控制更细粒度。
改造后代码:
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,实现声明式管理。
技术补丁包
-
Spring 事务传播机制核心原理 原理:基于 AOP 拦截方法调用,通过
TransactionSynchronizationManager使用 ThreadLocal 绑定事务资源(如 Connection),不同传播行为决定是加入现有事务还是新建/挂起事务。 设计动机:提供声明式事务管理,避免手动管理 Connection 和事务边界。 边界条件:REQUIRES_NEW 会挂起当前事务,若外层异常回滚,内层已提交无法撤销。 落地建议:优先使用 REQUIRED,仅在明确需要独立提交时使用 REQUIRES_NEW,并配合异步补偿机制。 -
TransactionSynchronizationManager 的 ThreadLocal 机制 原理:使用
ThreadLocal<Map<Object, Object>>存储当前线程的事务资源(dataSource -> Connection)、同步器列表、事务名称等。 设计动机:确保同一线程内多个事务拦截器能共享事务上下文,避免重复开启事务。 边界条件:异步线程、线程池复用会导致 ThreadLocal 污染或丢失,需手动清理或传递上下文。 落地建议:在异步场景中,使用TransactionContextHolder或消息头传递事务 ID,避免依赖 ThreadLocal。 -
REQUIRES_NEW 与数据库连接的绑定关系 原理:每次 REQUIRES_NEW 都会调用
DataSourceUtils.getConnection()获取新连接,执行setAutoCommit(false)开启新事务。 设计动机:确保事务隔离性,避免脏读、不可重复读。 边界条件:频繁使用 REQUIRES_NEW 会导致连接池压力增大,增加数据库负载。 落地建议:结合连接池监控(如 HikariCP 的 active connections),限制 REQUIRES_NEW 使用频率,或改用异步消息解耦。 -
声明式事务 vs 编程式事务的选型建议 原理:声明式基于
@Transactional+ AOP,编程式基于TransactionTemplate或TransactionManager手动控制。 设计动机:声明式简洁,编程式灵活,适用于复杂事务边界控制。 边界条件:声明式无法在同一个类内部调用时生效(AOP 代理限制),编程式可精确控制事务范围。 落地建议:简单场景用声明式,嵌套事务、条件回滚、多数据源等复杂场景优先使用编程式事务。