2. Spring 事务管理:@Transactional 注解、事务传播行为、事务隔离级别
在分布式系统与消息链路的调试中,事务边界是决定数据一致性的核心。Spring 通过 @Transactional 注解提供了声明式事务管理,让开发者能够以极简的代码控制事务的开启、提交与回滚。本章深入剖析该注解的底层机制、传播行为与隔离级别,为后续 Kafka 消息事务的衔接打下基础。
2.1 @Transactional 注解核心机制
@Transactional 本质上是 Spring AOP 的切面实现。当标注在类或方法上时,Spring 会为该类生成代理对象,在方法调用前后织入事务管理逻辑。
| 配置属性 | 默认值 | 说明 |
|---|---|---|
| propagation | Propagation.REQUIRED | 事务传播行为 |
| isolation | Isolation.DEFAULT | 事务隔离级别 |
| timeout | -1(无超时) | 事务超时秒数 |
| readOnly | false | 是否为只读事务 |
| rollbackFor | RuntimeException 和 Error | 指定触发回滚的异常类型 |
| noRollbackFor | 空 | 指定不触发回滚的异常类型 |
关键点:默认情况下,
@Transactional 只对非受检异常(RuntimeException 及其子类)和 Error 进行回滚。受检异常(如 SQLException)不会触发回滚,除非显式指定 rollbackFor。
@Service
public class OrderService {
@Transactional(rollbackFor = Exception.class)
public void createOrder(OrderDTO dto) {
// 1. 保存订单
orderDao.insert(dto);
// 2. 扣减库存(可能抛出受检异常)
inventoryService.deduct(dto.getProductId(), dto.getQuantity());
// 3. 发送消息(事务边界内)
messageService.sendOrderCreatedEvent(dto);
}
}
2.2 事务传播行为详解
传播行为定义了当多个事务方法相互调用时,事务如何扩散或隔离。这是调试消息链路时最容易出错的环节。
调试提示:在 Kafka 消息消费端,如果消费方法标注了
@Transactional,而内部又调用了其他事务方法,传播行为决定了这些操作是否属于同一个数据库事务。
| 传播行为 | 行为描述 | 消息链路调试场景 |
|---|---|---|
| REQUIRED | 支持当前事务,不存在则新建 | 消费消息时,数据库操作与消息确认在同一事务 |
| REQUIRES_NEW | 挂起当前事务,新建独立事务 | 发送 Kafka 消息前,先独立提交日志记录 |
| NESTED | 在当前事务内创建保存点 | 批量消息处理,单条失败仅回滚该条 |
| MANDATORY | 必须存在当前事务,否则抛异常 | 强制调用方已开启事务,防止无事务执行 |
| NEVER | 必须不存在当前事务,否则抛异常 | 非事务性操作(如缓存更新) |
2.3 事务隔离级别
隔离级别决定了事务之间数据可见性的程度。在消息链路中,消费端读取数据库时可能遇到脏读、不可重复读或幻读问题。
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 性能 |
|---|---|---|---|---|
| READ_UNCOMMITTED | 可能 | 可能 | 可能 | 最高 |
| READ_COMMITTED | 避免 | 可能 | 可能 | 高 |
| REPEATABLE_READ | 避免 | 避免 | 可能 | 中 |
| SERIALIZABLE | 避免 | 避免 | 避免 | 最低 |
消息链路陷阱:当 Kafka 消费者在
READ_COMMITTED 级别下读取订单表时,如果另一个事务正在更新订单状态但未提交,消费者可能读到旧数据(不可重复读)。对于要求严格顺序的消息处理,建议使用 REPEATABLE_READ 或结合乐观锁。
2.4 事务失效常见场景
在调试消息链路时,事务失效是最隐蔽的问题。以下场景会导致 @Transactional 不生效:
- 自调用:同一个类中的方法直接调用另一个
@Transactional方法,AOP 代理不生效。 - 非 public 方法:注解只能作用于 public 方法(基于代理机制)。
- 异常被捕获:方法内部 try-catch 吞掉了异常,事务无法感知。
- 数据库引擎不支持:如 MySQL 的 MyISAM 引擎不支持事务。
- 传播行为配置错误:如
SUPPORTS在没有事务时不会创建新事务。
// 错误示例:自调用导致事务失效
@Service
public class OrderService {
public void processOrder(OrderDTO dto) {
// 直接调用内部方法,AOP 代理不拦截
this.saveOrder(dto); // @Transactional 不生效!
}
@Transactional
public void saveOrder(OrderDTO dto) {
orderDao.insert(dto);
}
}
// 正确做法:注入自身代理
@Service
public class OrderService {
@Autowired
private OrderService self; // 注入代理对象
public void processOrder(OrderDTO dto) {
self.saveOrder(dto); // 通过代理调用,事务生效
}
@Transactional
public void saveOrder(OrderDTO dto) {
orderDao.insert(dto);
}
}
2.5 事务边界与消息发送的协调
在 Kafka 消息链路中,事务边界需要与消息发送时机精确对齐。常见模式有两种:
最佳实践:使用
TransactionSynchronizationManager.registerSynchronization() 注册事务同步回调,在事务成功提交后发送 Kafka 消息,确保数据与消息的最终一致性。
@Transactional
public void createOrderAndSendMessage(OrderDTO dto) {
// 1. 数据库操作
orderDao.insert(dto);
// 2. 注册事务同步回调
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronization() {
@Override
public void afterCommit() {
// 事务提交后才发送消息
kafkaTemplate.send("order-topic", dto.getOrderId());
}
}
);
}