Spring 里的 @Transactional 可以理解为一个声明式事务开关:加在方法或类上,Spring 容器在运行时通过代理对象自动管理数据库事务。跟编程式事务相比,它把 commit 和 rollback 的样板代码从业务方法中剥离出去,让代码更干净。但实际项目中,很多方法加了 @Transactional 却没有按预期回滚,问题往往出在对代理机制和属性配置的理解上。下面从底层机制、属性用法、失效场景和最佳实践四个方面展开说明。

一、@Transactional 的底层工作方式
Spring 的事务管理分为编程式事务和声明式事务两类。编程式事务需要手动获取事务管理器并调用 commit 或 rollback,代码繁琐但控制粒度细;声明式事务通过 AOP 代理把事务逻辑横切到目标方法前后,@Transactional 就是声明式事务的核心注解。它本身并不直接开启事务,而是告诉 Spring 容器:这个 Bean 的方法需要被事务代理包装。
具体流程是:Spring 在创建 Bean 时检测到类或方法上有 @Transactional,会为该 Bean 生成一个代理对象。外部调用方拿到的是代理对象,而不是原始对象。调用方法时,代理对象会先进入 TransactionInterceptor 拦截器,拦截器根据事务属性向 PlatformTransactionManager 申请开启事务,然后通过反射调用真实方法;方法正常返回则提交事务,抛出异常则根据回滚规则决定是否回滚。这也是为什么同类内部自调用会失效——因为 this 指向原始对象,绕过了代理。
事务管理器的选择取决于持久化技术。使用 JdbcTemplate 或 MyBatis 时通常对应 DataSourceTransactionManager;使用 Hibernate 或 JPA 时对应 JpaTransactionManager。Spring Boot 会根据 classpath 自动配置,一般无需手动注册。可以通过日志确认事务是否真正开启,例如在配置文件中设置 org.springframework.transaction 和 org.springframework.jdbc.datasource 的级别为 debug,日志里会出现 Creating new transaction 或 Rolling back transaction 等关键字。
logging:
level:
org.springframework.transaction: debug
org.springframework.jdbc.datasource: debug
二、核心属性与配置方式
@Transactional 支持多个属性,最常用的是 propagation、isolation、rollbackFor、readOnly 和 timeout。默认情况下,传播行为是 REQUIRED,也就是当前存在事务就加入,不存在就新建。这个属性适合大多数本地事务场景,但如果一笔订单保存后需要记录审计日志,而日志不能因为订单回滚而丢失,就可以把审计方法设置为 REQUIRES_NEW,让它在独立事务中提交。
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void writeAuditLog(AuditLog log) {
auditLogMapper.insert(log);
}
隔离级别一般使用数据库默认值即可,MySQL 默认为 REPEATABLE_READ。只有当业务对脏读、不可重复读、幻读有明确容忍度时,才需要显式调整。例如报表查询为了防止脏读可以设置 READ_COMMITTED。注意隔离级别最终受数据库支持能力限制,Spring 只是把值传递给数据库连接。
@Transactional(isolation = Isolation.READ_COMMITTED)
public Order readOrder(Long id) {
return orderMapper.selectById(id);
}
回滚规则是踩坑最多的地方:Spring 默认只对 RuntimeException 和 Error 进行回滚,对 checked exception 不会回滚。假如你写了一个导入方法,方法签名抛出 IOException,内部导入到一半失败,Spring 不会自动回滚已写入的数据。这时必须通过 rollbackFor 指定受检异常也要回滚。
@Transactional(rollbackFor = Exception.class)
public void importOrders(List<Order> orders) throws IOException {
for (Order order : orders) {
orderMapper.insert(order);
}
fileService.parse();
}
只读事务通过 readOnly = true 开启,主要作用是给数据库优化提供提示,同时防止在只读方法中执行更新操作。事务超时通过 timeout 设置,单位是秒,超过时间后事务会被强制回滚。这两个属性在查询类方法和批量处理场景中都比较实用。
@Transactional(readOnly = true, timeout = 10)
public List<Order> queryOrders() {
return orderMapper.selectList(null);
}
三、事务失效的常见场景与修复
第一种场景是同类方法自调用。示例中 processFromAnotherMethod 调用 this.createOrder,即便 createOrder 上有 @Transactional,调用也不会经过代理对象,因此事务不生效。修复方式有两种:一是把被调用方法拆到另一个 Service,二是注入自身代理对象后调用。
@Service
public class OrderService {
@Autowired
private OrderService self;
public void processFromAnotherMethod(Order order) {
self.createOrder(order);
}
@Transactional
public void createOrder(Order order) {
orderMapper.insert(order);
stockService.decrease(order.getProductId(), order.getCount());
}
}
第二种场景是方法不是 public。Spring 默认通过 JDK 动态代理或 CGLIB 子类代理来拦截方法,只有 public 方法能被拦截。如果注解加在 private 方法上,Spring 不会报错,但事务完全不会开启。第三种是异常被业务代码吞掉。下面这段代码捕获了异常但没有重新抛出,事务管理器看不到异常,最终仍会提交。
@Transactional
public void updateUser(User user) {
try {
userMapper.update(user);
accountService.deduct(user.getId(), 10);
} catch (Exception e) {
log.error("更新失败", e);
// 事务仍然会提交,除非手动设置回滚标记
}
}
修复方式是在 catch 中抛出 RuntimeException,或者调用 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly() 手动标记当前事务为只回滚。第四种场景是数据库存储引擎不支持事务,比如 MySQL 使用 MyISAM 表,事务不会产生实际回滚效果。业务表应使用 InnoDB 或其它支持事务的引擎。
第五种场景是多线程调用。事务资源绑定在当前线程上,如果在一个事务方法里通过线程池异步执行数据库操作,子线程不会自动加入当前事务。第六种是多数据源配置混乱。当项目里同时存在多个事务管理器时,必须通过 transactionManager 属性指定使用哪一个,否则 Spring 可能选择错误的事务管理器。第七种是类没有被 Spring 托管,比如自己 new 出来的对象或者忘记添加 Service 注解,自然也不会被代理。
四、调试技巧与落地建议
事务问题不要只靠猜,先打开 Spring 事务日志。日志中会明确打印方法名、事务名称、传播行为、隔离级别以及提交或回滚操作。根据日志可以快速判断代理是否生效,以及回滚是否因为异常类型不匹配而未触发。
在项目实践中,事务边界建议放在 Service 层的公共方法上,避免在 Controller 或 Mapper 方法上标注事务。一个事务方法不要执行过长时间,尤其不要在事务内调用远程 HTTP 接口或发送消息,否则会长时间占用数据库连接和行锁。查询类方法可以设置 readOnly = true,写入类方法控制好事务范围,不要把所有操作都揉在一个大事务里。
当业务逻辑非常复杂,声明式事务难以精确控制时,可以改用编程式事务模板 TransactionTemplate。它允许在方法内部显式决定提交、回滚和处理嵌套事务,代码可读性虽然略低,但控制粒度更高。
@Service
public class PaymentService {
private final TransactionTemplate transactionTemplate;
public PaymentService(PlatformTransactionManager transactionManager) {
this.transactionTemplate = new TransactionTemplate(transactionManager);
}
public void pay(Order order) {
transactionTemplate.execute(status -> {
orderMapper.updateStatus(order.getId(), "PAID");
walletService.deduct(order.getUserId(), order.getAmount());
return null;
});
}
}
掌握 @Transactional 的关键在于理解它依赖代理而不是魔法。只要把代理机制、属性取值和异常规则这三件事搞清楚,绝大多数事务问题都能在开发阶段发现并规避。

