Propagation.REQUIRES_NEW 嵌套后连接翻倍?事务传播级别的三个隐形成本陷阱
上篇文章解决了"同一个类里方法调方法不走代理"的问题——你在方法 A 上加 @Transactional,调本类方法 B,B 的事务配置被无视,退化为 outer 事务的一部分。
现在你知道了,把方法 B 抽到另一个 Service,给它加上 @Transactional(propagation = Propagation.REQUIRES_NEW),于是 B 应该有一个"全新独立"的事务。
这个直觉只对了一半。REQUIRES_NEW 确实是新事务,但它不是"免费"的。每嵌套调用一次,你的连接池可能被吃掉两份连接。 更隐蔽的是,如果 B 和 A 共享同一个 EntityManagerFactory,B 的回滚可能顺手把 A 的 PersistenceContext 也污染了——而你还在开心的 catch 异常,以为万事大吉。
REQUIRES_NEW 最贵的代价不是创建新事务,是外层连接挂起期间——它不吃不喝也不还。
【现场】一个 for 循环,HikariPool 爆了
事故全景
去年有一个线上工单:批量导入接口在数据量超过 50 条时随机超时。
代码结构是这样:
// OrderImportService.java
@Service
public class OrderImportService {
private final OrderValidator validator;
@Transactional
public void batchImport(List<OrderImportRequest> requests) {
for (OrderImportRequest req : requests) {
try {
validator.validateAndMark(req); // REQUIRES_NEW
} catch (InvalidOrderException e) {
log.warn("跳过无效订单: {}", req.getOrderNo());
}
}
}
}
// OrderValidator.java
@Service
public class OrderValidator {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void validateAndMark(OrderImportRequest req) {
// 读取配置表 + 校验规则 + 写入标记
orderConfigRepo.findByType(req.getType());
validationRepo.save(mark(req));
}
}
团队在 validateAndMark 上用了 REQUIRES_NEW,期望的是每条订单独立提交:校验失败的订单回滚自己的记录,不影响其他订单。
但线上跑 50 条订单时,到第 30 条左右接口就 hang 住了。
异常日志

maxPoolSize=10,active=10,idle=0——池子吃满了。
开发者第一反应:"连接池不够,调大。" 但调大到 20 后,能处理的条数从 30 条涨到了 60 条,依然有上限。
问题在哪?

问题不在池子小,在 REQUIRES_NEW 每个循环吃 2 个连接:
| 时机 | OrderImportService 事务(outer) | OrderValidator 事务(inner) |
|---|---|---|
| 第 1 条订单 | 持连接 C1,挂起 | 取连接 C2,执行,释放 C2 |
| 第 2 条订单 | 恢复 C1,再挂起 | 取连接 C3,执行,释放 C3 |
| ... | 循环 | 每次一条新连接 |
| 第 k 条订单 | C1 一直没还 | 取连接 C_{k+1} |
C1 被外层事务 挂起但没有归还连接池。每个并发的导入请求都占着 C1 不放,再吃一条临时连接给内层。10 个并发请求同时导入 → 每个请求占 C1 + C_临时 → 20 条连接被申请。maxPoolSize=10 时,10 个请求只有 5 个能拿到内层连接,其余 5 个的 inner 方法阻塞等待。
【拆解】REQUIRES_NEW 的三个隐形成本
陷阱一:挂起 = 持连接不还

这是最容易被忽略的代价。DataSourceTransactionManager 处理 REQUIRES_NEW 的核心逻辑:

关键行在 suspend → begin 之间:C1 只是从线程解绑,物理连接还在,没有归还连接池。这意味着 N 层嵌套 = N 个连接同时被占用。
这也是为什么调大连接池能缓解但治标不治本——连接池再大,也架不住每个请求按嵌套层数吃多份。
陷阱二:JPA PersistenceContext 污染——更隐蔽的连锁反应
假设你的代码涉及 JPA/Hibernate:
@Transactional
public void outerMethod(Long orderId) {
Order order = orderRepo.findById(orderId).orElseThrow();
order.setStatus("PROCESSING");
// Hibernate 此时已跟踪 order 为 "脏" 状态
auditService.auditChange(order); // REQUIRES_NEW
}
这里有个微妙的问题:Spring JpaTransactionManager 处理 REQUIRES_NEW 时,外层 EntityManager 被挂起,内层创建一个新的 EntityManager。内层执行完提交或回滚后,内层 EM 关闭,外层 EM 恢复。
但 Hibernate 的 PersistenceContext 是有状态的。如果内层方法操作了和外层相同的实体对象:
- 外层加载了 Order(managed 状态)
- 内层也加载了同一个 Order,做了修改
- 内层回滚 → 内层 EM 的 PersistenceContext 清空
- 外层恢复后 flush → Hibernate 尝试把外层的 order 变更写回数据库
- 但此时一级缓存中的 order 可能因为内层的操作而处于不一致状态
最典型的现象是 StaleObjectStateException 或 ConstraintViolationException,但错误堆栈指向的是外层方法最后一行,让人完全联想不到是内层 REQUIRES_NEW 的回滚导致的。
陷阱三:REQUIRES_NEW 在同类方法上退化为 REQUIRED(上一篇文章的延续)
这个问题和上篇文章本质相同——REQUIRES_NEW 也必须走代理才能生效。
@Service
public class OrderService {
@Transactional
public void process(Order order) {
saveOrder(order);
// 这里直接调本类方法
deductStock(order); // REQUIRES_NEW? 不,退化为 REQUIRED
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void deductStock(Order order) {
stockRepo.deduct(order.getProductId(), order.getQuantity());
}
}
Spring AOP 代理的工作机制决定了:process() 内部直接调 deductStock(),走的是 this.deductStock(),不是代理对象 → 事务拦截器不触发 → propagation 配置被无视 → 退化为外层事务(REQUIRED)。
你说:"我把 deductStock 抽到别的 Service 了,REQUIRES_NEW 生效了。那第一个陷阱还适用吗?"
适用。REQUIRES_NEW 生效 + 连接翻倍,是两个独立问题。生效解决的是"事务隔离",翻倍解决的是"资源消耗"。
【处方】选型决策树 + 正确配置
REQUIRES_NEW 不是银弹

| 你的需求 | 用哪个 | 原因 |
|---|---|---|
| 外层失败时内层也回滚 | REQUIRED(默认) |
一个事务,全部回滚,无额外连接开销 |
| 内层失败时不影响外层 | NESTED(JDBC savepoint) |
只回滚 savepoint 部分,外层继续,不额外吃连接 |
| 内层需要完全独立提交/回滚(不受外层影响) | REQUIRES_NEW |
独立事务,但注意连接翻倍 |
| 批量处理,每条记录独立提交 | TransactionTemplate + 手动控制 |
比 REQUIRES_NEW 更省连接,控制更精确 |
关键区别: NESTED 用 savepoint 实现,不申请新连接;REQUIRES_NEW 挂起外层 + 取新连接,连接成本翻倍。
当必须用 REQUIRES_NEW 时
如果业务真的需要完全独立的事务(比如审计日志必须单独提交,不随主事务回滚):
方案 A:增大连接池余量
spring.datasource.hikari.maximum-pool-size = (预期并发数 × 嵌套层数) + 其他连接消耗
例:20 并发 × 2 层嵌套 + 5 条管理查询 = 45。但这不是线性增长的——还要算上每个请求的执行时间。
更靠谱的方案 B:内外层用不同的连接池
配置两个 DataSource,内层专用一个较小的池,避免干扰主流程的连接。
spring:
datasource:
primary:
hikari.maximum-pool-size: 20
audit:
hikari.maximum-pool-size: 5
@Transactional(
value = "auditTransactionManager",
propagation = Propagation.REQUIRES_NEW
)
public void auditLog(...) { ... }
最推荐的方案 C:能用 TransactionTemplate 就别用 REQUIRES_NEW
@Service
public class BatchProcessor {
private final TransactionTemplate txTemplate;
public void batchProcess(List<Item> items) {
for (Item item : items) {
try {
txTemplate.execute(status -> {
processOneItem(item);
return null;
});
} catch (TransactionException e) {
log.error("处理失败: {}", item.getId());
}
}
}
}

TransactionTemplate 本质是每次新建事务,和 REQUIRES_NEW 效果一样,但:
- 没有"外层挂起"的开销——因为没有外层事务
- 连接用完即还,不会长时间持有
- 捕获异常后不会出现 UnexpectedRollbackException
快速自查:REQUIRES_NEW 用对了没有?
□ 目标方法是否在另一个 Service 中?(同类调用会退化为 REQUIRED)
□ 调用方是否真的需要内层完全独立?(NESTED 可能更省连接)
□ 连接池是否已按嵌套翻倍估算?(n 层嵌套 = n 倍单层连接消耗)
□ 如果有 JPA,内层是否操作了和外层相同的实体?(可能导致 PersistenceContext 污染)
【雷标】🔴 搜索你的项目
搜索关键词
propagation = Propagation.REQUIRES_NEW
@Transactional(propagation = Propagation.REQUIRES_NEW)
REQUIRES_NEW
搜索后检查
- REQUIRES_NEW 方法是否被另一个 @Transactional 方法调用? → 逐层数嵌套深度,评估连接翻倍风险
- REQUIRES_NEW 方法是否和调用方在同一个类中? → 退化为 REQUIRED,事务配置等于白写
- REQUIRES_NEW 方法和调用方是否操作了相同的实体? → 有 PersistenceContext 污染风险,考虑提取独立 DataSource
- 批量场景是否一定要用 REQUIRES_NEW? → 试试
TransactionTemplate或NESTED,连接消耗降低 50%+
📺 公众号「Ai拆代码的曹操」
🌟 知识星球「Ai拆代码的曹操」