跳到主要内容
版本:1.0

事务管理(@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