事务管理(@Transactional 深入)
事务是「要么全成功,要么全回滚」的保证。Spring 用 @Transactional 一个注解搞定声明式事务,但这个注解的坑也是面试和生产事故的重灾区。本篇讲清用法、原理和避坑。
基本用法
标注在 Service 的 public 方法上:
@Service
public class AccountService {
private final AccountMapper accountMapper;
public AccountService(AccountMapper accountMapper) {
this.accountMapper = accountMapper;
}
@Transactional(rollbackFor = Exception.class)
public void transfer(Long fromId, Long toId, BigDecimal amount) {
int rows = accountMapper.decrease(fromId, amount);
if (rows == 0) {
throw new BizException(2001, "余额不足");
}
accountMapper.increase(toId, amount);
// 方法正常返回 → 提交;抛出异常 → 回滚,两步操作同生共死
}
}
也可以标在类上,表示该类所有 public 方法都启用事务,方法上的配置会覆盖类上的。
注解属性全解
@Transactional(
propagation = Propagation.REQUIRED, // 传播行为
isolation = Isolation.READ_COMMITTED, // 隔离级别
timeout = 5, // 超时秒数
readOnly = false, // 只读事务(查询优化提示)
rollbackFor = Exception.class, // 哪些异常回滚
noRollbackFor = BizException.class // 哪些异常不回滚
)
回滚规则(第一坑)
默认只回滚 RuntimeException 和 Error,受检异常(如 IOException)不回滚!
// 危险:抛 IOException 时数据不会回滚
@Transactional
public void importData() throws IOException { }
// 正确:显式指定
@Transactional(rollbackFor = Exception.class)
public void importData() throws IOException { }
团队规范建议:一律写 rollbackFor = Exception.class,省心。
传播行为
当一个事务方法调用另一个事务方法,事务怎么传播?
| 传播行为 | 含义 | 典型场景 |
|---|---|---|
REQUIRED(默认) | 有事务就加入,没有就新建 | 绝大多数业务 |
REQUIRES_NEW | 挂起当前事务,新建独立事务 | 操作日志:主业务失败,日志仍要落库 |
NESTED | 嵌套事务(Savepoint),外层回滚内层必回滚,内层回滚不影响外层 | 部分子操作可独立回滚 |
SUPPORTS | 有事务就用,没有也行 | 查询方法 |
MANDATORY | 必须在事务中调用,否则报错 | 强制要求被事务包裹的底层方法 |
NOT_SUPPORTED / NEVER | 挂起事务 / 禁止事务 | 极少用 |
REQUIRES_NEW 经典例子——无论订单成败都记录日志:
@Transactional(rollbackFor = Exception.class)
public void createOrder(OrderDTO dto) {
orderMapper.insert(dto);
logService.saveLog("创建订单"); // 内部方法用 REQUIRES_NEW
inventoryMapper.deduct(dto); // 若这里失败,订单回滚,但日志已独立提交
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveLog(String content) {
logMapper.insert(content);
}
失效场景清单(事故高发区)
1. 同类自调用
public void methodA() {
this.methodB(); // this 是原始对象,不是代理,事务失效!
}
@Transactional
public void methodB() { }
原理:@Transactional 靠 AOP 代理实现,this. 调用绕过了代理对象。
解法(任选其一):
- 把
methodB挪到另一个 Service; - 注入自身:
@Autowired @Lazy private AccountService self;然后self.methodB(); ((AccountService) AopContext.currentProxy()).methodB()(需@EnableAspectJAutoProxy(exposeProxy = true))。
2. 异常被 try-catch 吞掉
@Transactional
public void method() {
try {
mapper.insert(a);
mapper.insert(b);
} catch (Exception e) {
log.error("失败", e); // 异常没抛出去,Spring 感知不到 → 不回滚!
}
}
解法:catch 后重新抛出,或手动 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();。
3. 其他常见失效
| 场景 | 原因 |
|---|---|
| 方法不是 public | 代理只拦截 public 方法 |
| 方法被 final / static 修饰 | 无法被代理覆写 |
| 数据库引擎不支持 | MySQL 的 MyISAM 无事务,需 InnoDB |
| 多线程内操作 | 事务绑定当前线程(ThreadLocal),子线程操作不在事务内 |
| 抛异常后又被外层 REQUIRED 加入的同一个事务 | 内层回滚会把整个事务标记 rollback-only,外层再提交会报 UnexpectedRollbackException |
只读事务与超时
@Transactional(readOnly = true)
public PageVO<UserVO> list(String name, int page, int size) { }
readOnly = true 给数据库和 ORM 一个优化提示(如 Hibernate 跳过快照比对),查询方法建议加上。
timeout 防止长事务拖垮连接池:
@Transactional(timeout = 3, rollbackFor = Exception.class)
事务与锁
事务不解决并发写冲突。秒杀扣库存这类场景,还需配合:
-- 方案一:乐观条件更新(推荐,无锁)
UPDATE account SET balance = balance - #{amount}
WHERE id = #{id} AND balance >= #{amount}
-- 影响行数 0 即余额不足
-- 方案二:悲观锁
SELECT * FROM account WHERE id = #{id} FOR UPDATE