声明式事务的执行路径
@Transactional 为一次经过 Spring 代理的方法调用附加事务属性。代理通过事务拦截器选择事务管理器、取得资源、执行目标方法,并根据返回或异常结果提交或回滚。
1. 注解只是事务属性来源
@Transactional
public Order create(CreateOrder command) {
Order order = repository.save(command.toOrder());
inventory.reserve(order.items());
return order;
}
容器识别事务注解,为 Bean 创建代理,并将 TransactionInterceptor 加入拦截链。只有调用进入这份代理,注解才参与执行。
注解可以声明传播方式、隔离级别、超时、只读提示和回滚规则;真正能否支持这些属性,由所选 TransactionManager 和底层资源决定。
2. 拦截器围绕目标方法管理事务
简化路径如下:
调用代理方法
→ 解析 TransactionAttribute
→ 选择 TransactionManager
→ 获取或加入事务
→ 调用目标方法
→ 正常返回:提交
→ 异常退出:按回滚规则提交或回滚
→ 清理线程绑定资源
PlatformTransactionManager 是通用抽象;JDBC、JPA、JTA 和响应式事务使用的具体实现与上下文传播方式不同。
3. JDBC 连接通常绑定到当前事务上下文
事务开始后,Spring 获取连接并把它与当前执行上下文关联。同一事务内通过 Spring 数据访问基础设施取得连接时,会复用这项资源;事务结束后提交或回滚并归还连接池。
业务代码自行调用独立 DataSource 或开启新线程,可能取得另一条连接,不会自动加入原事务。代理事务通常沿当前线程传播,线程切换必须使用框架明确支持的响应式或上下文协议。
4. 提交也可能失败
目标方法正常返回,只说明业务代码没有抛出异常。数据库约束、连接故障或全局事务协调可能在 flush 或 commit 阶段才失败。
不要在事务方法返回后立即假定外部副作用都已经完成。发送消息、调用远程服务与本地数据库提交无法由单个 JDBC 事务统一原子化,需要 Outbox、幂等或补偿方案。
5. 事务边界应围绕业务不变量
事务过小会把需要共同提交的状态拆开;事务过大则长时间占用连接和锁,并把远程调用延迟放进数据库事务。
典型边界是一个应用服务方法:读取完成决策所需状态,执行数据库更新,然后提交。慢网络调用尽量移出本地事务,并通过可恢复的状态连接前后步骤。
6. 编程式事务适合动态边界
需要精确控制重试范围、在循环中逐批提交或避免代理自调用时,可以使用 TransactionTemplate:
return transactionTemplate.execute(status -> repository.save(order));
它让边界直接出现在代码中,代价是业务逻辑显式依赖 Spring 事务 API。声明式与编程式应按边界清晰度选择。
7. 常见问题
7.1 在事务方法中调用远程接口,远程操作会一起回滚吗
不会。本地事务管理器只管理它负责的资源。远程请求已成功后,本地回滚不会撤销对方状态。
7.2 readOnly = true 能保证方法不写数据库吗
通常是优化提示和框架配置,具体数据库与驱动支持不同,不能当成业务安全边界。写权限仍需数据库授权和代码约束。
8. 面试题
8.1 @Transactional 从调用到提交经历什么
出现公司:美团、阿里云
考察重点
- 代理、事务属性和 TransactionInterceptor。
- 事务管理器与资源绑定。
- 提交阶段失败和远程调用边界。
相关内容:第 1 节“注解只是事务属性来源”至第 5 节“事务边界应围绕业务不变量”。
参考回答
容器为事务 Bean 创建代理,调用进入 TransactionInterceptor 后解析方法的事务属性并选择 TransactionManager。管理器创建或加入事务,绑定 JDBC 连接等资源,拦截器再调用目标方法;正常结果尝试提交,异常则按回滚规则处理,最后清理资源。
提交本身仍可能因 flush、约束或连接失败。事务只覆盖管理器负责的本地资源,远程调用和消息发送不能靠扩大 @Transactional 自动回滚。