Spring 七种事务传播行为详解
Spring 七种事务传播行为详解
事务传播行为(Propagation)定义了当一个有事务注解的方法被另一个方法调用时,事务应该如何传播。Spring 通过
@Transactional(propagation = Propagation.XXX)来控制。
底层原理: Spring 事务通过
TransactionSynchronizationManager将当前事务绑定在线程本地变量(ThreadLocal)上。判断”当前是否存在事务”,本质上就是检查当前线程的 ThreadLocal 中是否已绑定了一个活跃的数据库连接。理解这一点,很多”失效”场景的原因就迎刃而解了。
Spring 共提供七种传播行为,核心问题只有两个:
- 当前线程是否已存在事务?
- 被调用方是否要加入、新建,还是拒绝?
一、REQUIRED(默认)
规则:有事务就加入,没有就新建。
这是最常用的传播行为,也是
@Transactional的默认值。调用方有事务,被调用方加入同一事务,任何一方抛出异常都会导致整个事务回滚。
@Service
public class OrderService {
@Resource
private PayService payService;
@Transactional // 开启事务 A
public void createOrder(Order order) {
orderDao.insert(order);
payService.pay(order); // 加入事务 A,共用同一连接
// 任意一步异常 → 事务 A 整体回滚
}
}
@Service
public class PayService {
@Transactional(propagation = Propagation.REQUIRED) // 加入外层事务 A
public void pay(Order order) {
payDao.deduct(order.getAmount());
}
}
高频陷阱:内部异常被 catch 后仍抛
UnexpectedRollbackException原因:内部方法抛出异常时,Spring 将当前事务标记为
rollback-only(通过TransactionStatus.setRollbackOnly())。即使外层 catch 住了异常,
rollback-only标记不可撤销。外层提交时,Spring 发现标记已设置,强制回滚并抛出UnexpectedRollbackException。
@Transactional
public void createOrder(Order order) {
orderDao.insert(order);
try {
payService.pay(order); // 内部抛异常 → 事务被标记 rollback-only
} catch (Exception e) {
log.error("支付失败,降级处理", e);
// 以为 catch 住了就没事,实际上事务已不可提交
}
// 走到这里试图提交 → 抛 UnexpectedRollbackException!
}
解决方案: 需要独立提交的子操作改用
REQUIRES_NEW,让其运行在独立事务中,异常不污染外层事务状态。
二、REQUIRES_NEW
规则:无论如何都新建事务,将当前事务(如果有)挂起。
内外两个事务完全独立,使用不同的数据库连接。外部事务回滚不影响内部;内部事务回滚不影响外部(前提是内部异常被外部捕获)。
典型场景:操作日志、审计记录、补偿操作。 无论主业务成功与否,日志必须单独落库。
@Service
public class OrderService {
@Resource
private AuditLogService auditLogService;
@Transactional
public void createOrder(Order order) {
orderDao.insert(order);
auditLogService.log("创建订单", order.getId()); // 独立事务提交
if (order.getAmount() < 0) {
throw new IllegalArgumentException("金额不合法");
// orderDao.insert 回滚,但日志已经单独提交,不受影响
}
}
}
@Service
public class AuditLogService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void log(String action, Long bizId) {
logDao.insert(new AuditLog(action, bizId)); // 独立事务,必定提交
}
}
高频陷阱:REQUIRES_NEW 可能引发死锁
当内外两个事务操作的是同一行数据时,极易发生死锁:
- 外层事务持有 row A 的行锁(还未提交)
- 内层
REQUIRES_NEW开启新事务,尝试获取 row A 的锁- 内层等待外层释放锁,外层又等待内层完成 → 死锁
@Transactional
public void updateOrder(Long orderId) {
orderDao.updateStatus(orderId, "PROCESSING"); // 外层持有 orderId 行锁
// 内层 REQUIRES_NEW 开新连接,也要更新同一行 → 等待外层锁 → 死锁!
auditService.logStatusChange(orderId, "PROCESSING");
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logStatusChange(Long orderId, String status) {
// 这里如果也操作了 orderId 对应的行,就会死锁
orderDao.insertLog(orderId, status); // 假设这张表外键关联了 order 表且有锁
}
避免方式: 确保内层
REQUIRES_NEW操作的表/行与外层无重叠;或将日志写入完全独立的表。
注意:
REQUIRES_NEW每次都新建数据库连接,高并发频繁调用会导致连接池耗尽。建议将日志、审计类操作异步化(MQ 或@Async),而非直接用REQUIRES_NEW同步写。
三、SUPPORTS
规则:有事务就加入,没有事务就以非事务方式运行。
自身不主动开启事务,行为完全随调用方走。
@Service
public class ProductService {
// 被事务方法调用时加入事务,单独调用时非事务执行
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
public Product getById(Long id) {
return productDao.findById(id);
}
}
实际项目中几乎不用。 行为不确定(有没有事务取决于调用方),可测试性差。
纯查询用
@Transactional(readOnly = true),行为明确,还能让数据库做读优化(跳过 undo log 生成等)。
四、NOT_SUPPORTED
规则:以非事务方式运行,如果当前存在事务则将其挂起。
强制让某段逻辑在事务外执行,常用于运行时间不确定的外部调用(HTTP、MQ 消费确认、读缓存),避免长时间占用数据库连接导致连接池耗尽。
@Service
public class NotifyService {
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void sendSms(String phone, String content) {
// 外部事务已挂起,此方法不占用数据库连接
// 即使 smsGateway.send 耗时 5 秒,也不会占用连接池资源
smsGateway.send(phone, content);
}
}
注意: 挂起事务期间,外层持有的数据库行锁依然存在(连接归还给连接池,但事务的锁元数据保留在数据库端)。
如果挂起时间过长,其他等待该锁的事务会超时。使用前需评估锁的影响范围。
五、MANDATORY
规则:必须在事务中运行,如果当前没有事务则抛出
IllegalTransactionStateException。
一种防御性编程手段:方法在设计上要求调用方必须开启事务,用运行时异常代替文档约定,防止误用。
@Service
public class InventoryService {
// 扣减库存必须有事务保护,否则立刻报错,让问题暴露在测试阶段
@Transactional(propagation = Propagation.MANDATORY)
public void deductStock(Long productId, int quantity) {
int affected = inventoryDao.deduct(productId, quantity);
if (affected == 0) {
throw new StockNotEnoughException("库存不足");
}
}
}
@Service
public class OrderService {
@Transactional // 必须加,否则调用 deductStock 时抛 IllegalTransactionStateException
public void createOrder(Order order) {
inventoryService.deductStock(order.getProductId(), order.getQuantity());
orderDao.insert(order);
}
}
这是一种合约式设计:方法声明”我只能在事务中被调用”,如果调用方违约,立即暴露问题。在多人协作的大型项目中能有效防止漏加事务的 bug。
六、NEVER
规则:必须在非事务环境中运行,如果当前存在事务则抛出
IllegalTransactionStateException。
与
MANDATORY正好相反,强制要求调用方不能有事务。适合不能参与事务的操作(如某些统计查询会持有全表扫描锁,必须在事务外执行)。
@Service
public class StatisticsService {
@Transactional(propagation = Propagation.NEVER)
public StatisticsResult calcDailyReport(LocalDate date) {
// 全表聚合查询,若在事务中执行会长时间持锁
return statisticsDao.aggregateByDate(date);
}
}
实际项目中极少使用。 绝大多数情况下,通过 code review 和架构规范约定即可,不必依赖运行时抛错来保障。
七、NESTED
规则:如果当前存在事务,则创建一个嵌套事务(通过 Savepoint 实现);如果不存在,则与 REQUIRED 行为一致。
NESTED是七种传播行为中最特殊的一种,利用数据库的保存点(Savepoint)机制实现”部分回滚”:
- 内部事务回滚 → 回滚到 Savepoint,不影响外部事务
- 外部事务回滚 → 内部事务也跟着回滚(因为共用同一个物理事务)
@Service
public class OrderService {
@Resource
private CouponService couponService;
@Transactional
public void createOrder(Order order) {
orderDao.insert(order); // 外层事务
try {
couponService.useCoupon(order); // 嵌套事务,创建 Savepoint
} catch (CouponException e) {
// 内层回滚到 Savepoint,优惠券使用失败
// 外层事务不受影响,订单照样创建成功
log.warn("优惠券使用失败,继续下单: {}", e.getMessage());
order.setActualAmount(order.getAmount()); // 按原价走
}
payDao.deduct(order.getActualAmount()); // 外层继续执行
}
}
@Service
public class CouponService {
@Transactional(propagation = Propagation.NESTED)
public void useCoupon(Order order) {
couponDao.markUsed(order.getCouponId());
if (couponExpired(order.getCouponId())) {
throw new CouponException("优惠券已过期"); // 只回滚到 Savepoint
}
order.setActualAmount(order.getAmount() - coupon.getDiscount());
}
}
NESTED vs REQUIRES_NEW 核心区别:
| 对比项 | REQUIRES_NEW | NESTED |
|---|---|---|
| 物理事务 | 两个独立事务,各自提交/回滚 | 同一个物理事务 |
| 外层回滚影响内层? | 不影响(内层已提交) | 影响(内层跟着回滚) |
| 内层回滚影响外层? | 不影响(独立事务) | 不影响(回到 Savepoint) |
| 是否新建连接 | 是(新连接) | 否(同一连接) |
| 典型场景 | 日志、审计必须独立持久化 | 子操作允许失败、主流程继续 |
注意:
NESTED依赖底层DataSourceTransactionManager对 Savepoint 的支持。使用JpaTransactionManager(Spring Data JPA 默认)时,NESTED不生效,会退化为REQUIRED行为,使用前务必确认事务管理器类型。MySQL InnoDB 原生支持 Savepoint。
八、七种传播行为速查
| 传播行为 | 有外部事务 | 无外部事务 | 典型场景 |
|---|---|---|---|
REQUIRED |
加入 | 新建 | 默认,绝大多数业务方法 |
REQUIRES_NEW |
挂起外部,新建 | 新建 | 操作日志、审计、补偿 |
SUPPORTS |
加入 | 非事务运行 | 可选事务的查询(实际少用) |
NOT_SUPPORTED |
挂起外部,非事务 | 非事务运行 | 长耗时外部调用 |
MANDATORY |
加入 | 抛出异常 | 强制调用方必须开事务 |
NEVER |
抛出异常 | 非事务运行 | 禁止在事务中调用 |
NESTED |
嵌套事务(Savepoint) | 新建 | 允许子操作失败、主流程继续 |
九、@Transactional 失效的常见场景
这是面试高频题,也是工作中 bug 的高发区,务必牢记。
场景一:同类内部调用(最常见)
@Transactional基于 Spring AOP 代理实现,同类内部调用走this.method(),绕过代理,注解失效。
@Service
public class OrderService {
@Transactional
public void createOrder(Order order) {
orderDao.insert(order);
this.sendNotify(order); // 走 this,绕过代理!sendNotify 的事务注解无效
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void sendNotify(Order order) {
// 实际上没有开启新事务,而是加入了 createOrder 的事务
notifyDao.insert(order);
}
}
解决方案: 将
sendNotify拆分到独立的 Bean;或通过AopContext.currentProxy()获取代理对象调用(需开启@EnableAspectJAutoProxy(exposeProxy = true))。
场景二:方法非 public
Spring AOP 基于动态代理,只能拦截
public方法。private、protected、default方法上的@Transactional静默失效,不报错。
@Service
public class OrderService {
@Transactional // 静默失效!方法不是 public
private void doInsert(Order order) {
orderDao.insert(order);
}
}
场景三:Checked 异常默认不回滚
Spring 默认只对
RuntimeException及其子类回滚,IOException、SQLException等 Checked 异常不触发回滚。
@Transactional // 遇到 IOException 不会回滚!
public void importData(File file) throws IOException {
orderDao.insert(parse(file)); // 已写入
fileProcessor.process(file); // 抛 IOException → 不回滚,数据不一致!
}
// 正确写法:显式指定
@Transactional(rollbackFor = Exception.class)
public void importData(File file) throws IOException {
orderDao.insert(parse(file));
fileProcessor.process(file);
}
场景四:异常被吞掉
@Transactional
public void createOrder(Order order) {
try {
orderDao.insert(order);
payService.pay(order);
} catch (Exception e) {
log.error("失败", e);
// 异常被吃掉,Spring 感知不到异常,事务正常提交
// 但 pay 可能只执行了一半!
}
}
规则:让 Spring 感知到异常,事务才能正确回滚。 捕获后要么重新抛出,要么手动调用
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。
场景五:@Async + @Transactional 混用
@Async让方法在新线程执行,新线程没有父线程的ThreadLocal事务上下文,@Transactional完全独立,不会加入调用方的事务。
@Transactional
public void createOrder(Order order) {
orderDao.insert(order);
notifyService.sendAsync(order); // 新线程,独立事务(或无事务)
if (someCondition) {
throw new RuntimeException("回滚");
// createOrder 回滚,但 sendAsync 的操作已在独立线程提交,无法撤销
}
}
@Async
@Transactional
public void sendAsync(Order order) {
// 这里的事务与 createOrder 完全无关
notifyDao.insert(order);
}
场景六:Bean 未被 Spring 管理
@Transactional要求方法所在的类必须是 Spring 管理的 Bean(通过@Service、@Component等注册)。直接new出来的对象,事务注解无效。
// 错误:直接 new,不经过 Spring 容器
OrderService service = new OrderService();
service.createOrder(order); // 事务失效
十、常见面试追问
Q:为什么同类内部调用事务失效?
Spring 事务通过 CGLIB 或 JDK 动态代理生成代理类,事务逻辑在代理类中。同类内部方法互相调用走的是
this引用,即原始对象,不经过代理,事务切面无法拦截。本质是 AOP 的实现限制:代理只能在 Bean 的入口处织入,内部调用穿透代理直接执行目标方法。
Q:
@Transactional加在接口上有效吗?如果使用 JDK 动态代理(实现了接口),可以生效;如果使用 CGLIB 代理(没有接口或强制使用 CGLIB),加在接口上的
@Transactional不生效。最佳实践:统一加在实现类的方法上,避免代理方式不同带来的差异。
Q:REQUIRED 下内部异常被外部 catch,为什么还会抛
UnexpectedRollbackException?Spring 的事务拦截器在捕获到异常后会调用
doSetRollbackOnly()标记当前事务,该标记一旦设置不可撤销。外层捕获异常只是阻止了异常传播,但无法清除已设置的rollback-only标记。提交时AbstractPlatformTransactionManager检查到标记,执行回滚并抛出UnexpectedRollbackException。
Q:NESTED 和 REQUIRES_NEW 如何选择?
- 子操作失败允许降级、主流程继续,但主流程回滚时子操作也应撤销 → 用
NESTED- 子操作必须独立持久化(无论主流程成功与否)→ 用
REQUIRES_NEW- 使用 JPA/Hibernate 作为 ORM 框架 → 只能用
REQUIRES_NEW(NESTED不支持JpaTransactionManager)
Q:事务中执行了一个非常慢的操作,会有什么问题?
事务期间持有数据库连接不释放,长事务会导致:
- 连接池耗尽:大量请求等待获取连接,接口超时
- 锁持有时间过长:其他事务等锁,并发性能下降
- undo log 膨胀:MySQL 中长事务导致 undo log 无法及时清理,占用大量磁盘
优化方向: 将非数据库操作(HTTP 调用、文件处理、缓存预热)移到事务外;大批量写操作分批提交;核心写操作用
REQUIRES_NEW单独提交,其余异步化。