声明式事务
约 1589 字大约 5 分钟
布欧-Lewyon
2026-05-15
首页 › Spring Boot › 数据访问 › 声明式事务
Spring 通过 @Transactional 注解实现声明式事务管理——无需手动调用 commit / rollback,框架根据方法返回或异常自动处理。
@Transactional 基础
@Service
public class UserService {
private final UserRepository userRepository;
private final AuditLogRepository auditLogRepository;
@Transactional
public User createUser(UserCreateRequest request) {
User user = new User(request.getUsername(), request.getAge());
userRepository.save(user);
auditLogRepository.save(new AuditLog("CREATE_USER", user.getId()));
// 方法正常返回 → 自动 commit
// 抛出 RuntimeException → 自动 rollback
return user;
}
}回滚规则
默认行为:
- 运行时异常(
RuntimeException及其子类)→ 回滚 - 受检异常(
Exception但不是RuntimeException)→ 不回滚
@Transactional(rollbackFor = Exception.class) // 所有异常都回滚
public void transfer(Long fromId, Long toId, BigDecimal amount) throws InsufficientBalanceException {
// ...
}
@Transactional(noRollbackFor = {BusinessException.class}) // 特定异常不回滚
public void someMethod() {
// ...
}事务传播行为
当一个事务方法调用另一个事务方法时,propagation 决定如何参与事务:
@Transactional(propagation = Propagation.REQUIRED) // 默认:加入当前事务,没有则新建
@Transactional(propagation = Propagation.REQUIRES_NEW) // 挂起当前事务,新建一个独立事务
@Transactional(propagation = Propagation.NESTED) // 嵌套事务(依赖 JDBC savepoint)
@Transactional(propagation = Propagation.MANDATORY) // 必须存在当前事务,否则抛异常
@Transactional(propagation = Propagation.NEVER) // 不能在事务中运行
@Transactional(propagation = Propagation.SUPPORTS) // 有则参与,无则以非事务运行
@Transactional(propagation = Propagation.NOT_SUPPORTED)// 以非事务方式运行REQUIRES_NEW 示例
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final AuditService auditService;
@Transactional
public void createOrder(Order order) {
orderRepository.save(order);
// 即使 auditService 中的事务回滚,当前订单入库不受影响
auditService.log("ORDER_CREATED", order.getId());
}
}
@Service
public class AuditService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void log(String action, Long targetId) {
// 独立事务,外部回滚不影响此处提交
}
}进阶阅读:TransactionInterceptor 调用链
L1/L2 读者理解回滚规则与传播行为即可。
Spring 声明式事务基于 AOP 代理实现,核心拦截器是 TransactionInterceptor(继承 TransactionAspectSupport)。其 invokeWithinTransaction() 方法是事务管理的核心入口:
AOP 代理创建时机
代理在 AbstractAutoProxyCreator.postProcessAfterInitialization() 中创建。在 refresh() 的 finishBeanFactoryInitialization 阶段,每个单例 Bean 初始化后都会被 AbstractAutoProxyCreator.wrapIfNecessary() "审查":
AbstractAutoProxyCreator.wrapIfNecessary(bean, beanName, cacheKey)
│
└── 获取该 Bean 的 Advisor 列表
└── DefaultAdvisorAutoProxyCreator 扫描所有 Advisor Bean
├── BeanFactoryTransactionAttributeSourceAdvisor(事务)
│ └── matches(method, targetClass) → 方法/类上有 @Transactional 则匹配
├── AsyncAnnotationAdvisor(异步)
└── ...
│
└── 匹配到任何 Advisor → 创建 AOP 代理(JDK 动态代理 / CGLIB)
└── 返回代理对象替代原始 Bean这就是为什么同类自调用 this.methodB() 不经过事务——this 是原始 Bean 的引用,不是代理对象。只有通过依赖注入获取的引用(也是代理对象的引用)才会触发 TransactionInterceptor。
生产意识:
AbstractTransactionManager在processRollback()中有一个关键行为:如果 rollback 时发现已经发生了commit(例如在afterCompletion回调中),会抛出UnexpectedRollbackException——这常发生在嵌套事务场景:外部事务期望回滚,但内部REQUIRES_NEW事务已经独立提交。这是 Spring 事务设计上的保护机制——外部设置rollbackOnly=true后,如果内部REQUIRES_NEW的事务成功提交并清理了回滚标记,外部最终 commit 时发现rollbackOnly=true则会抛出UnexpectedRollbackException。在事务内进行远程调用(HTTP、RPC、消息发送)是常见的高危操作:远程调用超时本身不会回滚事务,但如果远程调用抛出了RuntimeException,事务会回滚。更危险的是远程调用成功、后续数据库操作失败——远程操作无法被撤销。分布式事务需要通过 TCC、Saga 或最终一致性模式解决,不在本文范围内。
1. 在同一个类中调用事务方法
@Service
public class UserService {
@Transactional
public void methodA() {
// 执行操作
this.methodB(); // 直接调用,@Transactional 不生效!
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {
// 期望 REQUIRES_NEW,实际 AOP 代理拦截不到
}
}原因:Spring 的声明式事务通过 AOP 代理实现。同一个类内部调用 this.methodB() 不经过代理,注解不会生效。解决方案:
- 将
methodB提取到另一个@Service中注入调用。 - 或者
@Autowired自身代理(@EnableAspectJAutoProxy(exposeProxy = true)+AopContext.currentProxy())。
2. 捕获异常未重新抛出
@Transactional
public void badMethod() {
try {
// 数据库操作
} catch (Exception e) {
log.error("出错了", e);
// 没有抛出异常 → 事务正常 commit,数据写入!
}
}解决方案:捕获业务异常后重新抛出运行时异常,或手动标记回滚:TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。
3. 事务超时
长时间运行的事务会持有数据库连接直到提交,可能导致连接池耗尽:
@Transactional(timeout = 10) // 10 秒后自动回滚
public void batchProcess(List<Long> ids) {
// 如果 10 秒没执行完,自动回滚,释放连接
}生产意识:事务超时时长应根据业务评估设定,不设超时的长事务是连接池杀手。特别是事务中混入了远程调用(HTTP、RPC)的场景——远程调用未设置超时或超时时间过长,导致事务线程挂起等待,连接不释放,最终池子被耗尽。原则:事务内不做远程调用。如果无法避免,确保远程调用设置了合理的 read timeout(建议 < 事务超时的一半),并使用
@Transactional(timeout)兜底。
小结
@Transactional声明式管理事务边界,运行时异常默认回滚,受检异常不回滚。rollbackFor/noRollbackFor自定义回滚规则。Propagation.REQUIRED(默认)加入当前事务,REQUIRES_NEW新开独立事务。- 易错:同一个类内部调用事务方法不生效(AOP 代理限制);捕获异常后不重新抛出会导致事务不回滚。
- 思考任务:编写一个转账方法,包含扣款和入账两次数据库操作,用
@Transactional保证原子性,手动触发异常验证余额是否正确回滚。
上一节:Spring Data JPA
下一节:数据库迁移
