
Spring事务面试总在自调用上翻车?4个场景把@Transactional、回滚和传播边界讲清
聚焦 Spring `@Transactional` 面试中的自调用、异常回滚、传播行为和连接资源边界,给出 4 个代码场景与可直接复述的排查顺序。
先记住这 4 句话,面试官再怎么换场景,你都能顺着边界答下去:
@Transactional默认是PROPAGATION_REQUIRED:没有外层事务就新建,有外层事务就加入。- Spring 默认用代理模式处理声明式事务,只有经过代理的外部调用才会触发事务拦截。
- 同一个对象里用
this.xxx()或隐式自调用,不会再次经过代理;因此内层方法上的@Transactional可能完全不生效。 - 默认情况下,
RuntimeException和Error触发回滚,checked exception 不会自动触发回滚。1
@Transactional 面试真正容易扣分的地方,不是背出注解全名,而是能不能说明:调用从哪里进来、实际是不是同一个物理事务、异常有没有把事务标记为回滚,以及这个选择会不会耗尽连接资源。场景一:同类自调用,注解为什么像失效了
先看一段很容易被误判的代码:
@Service
public class OrderService {
public void submit(OrderRequest request) {
createOrder(request); // 同一个对象内部调用
}
@Transactional
public void createOrder(OrderRequest request) {
orderRepository.save(request);
paymentRepository.createPending(request.orderId());
}
}如果 Controller 调用的是
orderService.submit(),submit() 再在类内部调用 createOrder(),这次调用走的是目标对象自身,不是 Spring 代理。createOrder() 上的事务 advice 不会被触发,方法里的两次写入就不能按你以为的方式自动包进一个新事务。Spring 对 proxy-based AOP 的解释很明确:目标对象内部通过 this 的调用会绕过代理;@Transactional 默认也采用 proxy 模式。12面试时不要只说「自调用事务失效」。把调用路径补全:
外部对象拿到的是代理引用,外部调用可以触发事务拦截;同一个目标对象内部的this.createOrder()不经过代理,所以createOrder()上的@Transactional不会在这里创建事务。
怎么改,才不是只会背结论
优先把事务边界放在真正的入口方法上:
@Transactional
public void submit(OrderRequest request) {
createOrder(request);
}如果
submit() 本身就是一次完整的下单事务,这种写法最直观。内部方法只是同一事务里的普通调用,不需要再依赖第二次代理拦截。如果内层必须拥有独立的事务边界,把它拆到另一个 Bean:
@Service
public class OrderAppService {
private final OrderTxService orderTxService;
public void submit(OrderRequest request) {
orderTxService.createOrder(request); // 经过另一个 Bean 的代理
}
}
@Service
public class OrderTxService {
@Transactional
public void createOrder(OrderRequest request) {
orderRepository.save(request);
paymentRepository.createPending(request.orderId());
}
}Spring 文档也列出 self injection 和
AopContext.currentProxy() 等处理方式,但把 AopContext.currentProxy() 标为不推荐的最后手段。面试回答里,先给「重构事务边界」或「拆分 Bean」通常比背 API 更稳。2场景二:抛了异常,为什么数据库还提交了
下面这段代码有一个经常被忽略的词:
IOException 是 checked exception。@Transactional
public void uploadAndSave(File file) throws IOException {
documentRepository.insert(file.name());
storageClient.upload(file); // 抛 IOException
}Spring 默认对
RuntimeException 和 Error 回滚,对 checked exception 不回滚。也就是说,除非你显式配置回滚规则,这段代码抛出 IOException 时,事务可能仍按提交路径结束,数据库里已经插入的记录不会因为这个 checked exception 自动撤销。1如果业务定义是「文件没上传成功,数据库记录也不能保留」,可以把规则写出来:
@Transactional(rollbackFor = IOException.class)
public void uploadAndSave(File file) throws IOException {
documentRepository.insert(file.name());
storageClient.upload(file);
}这里要区分两件事:
rollbackFor表达的是业务上的回滚边界,不是把所有异常都机械改成回滚。- 如果方法捕获异常后继续返回成功,事务拦截器看到的可能已经是正常返回;你要先决定失败是否应该让调用方感知,再决定是声明回滚规则、重新抛出异常,还是明确保存失败状态。
面试官继续问「那
Exception 都回滚吗」,可以这样答:默认不是。Spring 默认回滚RuntimeException和Error,checked exception 需要用rollbackFor等规则显式配置;最终要按业务原子性决定,而不是只按异常类名套模板。
场景三:内层失败被 catch 了,外层为什么仍然提交不了
这段代码比「异常会不会回滚」更容易暴露真实理解:
@Service
public class OrderAppService {
@Transactional
public void placeOrder(OrderRequest request) {
orderRepository.save(request);
try {
stockService.deduct(request.sku(), request.quantity());
} catch (RuntimeException ex) {
log.warn("库存扣减失败", ex);
// 这里选择继续做其他事情
}
auditRepository.save("尝试下单");
}
}
@Service
public class StockService {
@Transactional // 默认 REQUIRED
public void deduct(String sku, int quantity) {
stockRepository.decrease(sku, quantity);
throw new RuntimeException("库存校验失败");
}
}placeOrder() 和 deduct() 都走代理,且内层默认是 PROPAGATION_REQUIRED。在已有外层事务时,内层加入外层,通常映射到同一个物理事务。内层异常可能把这个事务标记为 rollback-only;即使外层 catch 住异常并继续执行,提交时仍可能抛出 UnexpectedRollbackException,因为这个物理事务已经不能安全提交。3这时不要急着改成
REQUIRES_NEW。先问清楚失败语义:- 库存扣减失败就应该让下单失败:让异常继续向外抛,或者在外层按明确的失败状态结束,不要 catch 后伪装成成功。
- 库存失败只是可记录的旁路事件:把旁路记录拆到独立 Bean,再考虑独立事务;不要把「catch 住」误当成「事务恢复了」。
- 只想让外层继续,却仍然希望这次数据库操作提交:必须重新设计事务边界,不能靠同一物理事务里的异常捕获解决。
可以把这段追问压缩成一句话:
REQUIRED不是每个方法各开一笔事务。内层加入外层后,内层的回滚标记会影响同一个物理事务;catch 住 Java 异常,不等于清除了rollback-only。
场景四:REQUIRES_NEW 不是「嵌套事务」,还会多占资源
典型需求是:订单主事务失败时,仍想留下独立的审计记录。
@Service
public class OrderAppService {
private final AuditService auditService;
@Transactional
public void placeOrder(OrderRequest request) {
orderRepository.save(request);
try {
paymentService.charge(request);
} catch (RuntimeException ex) {
auditService.record("支付失败", request.orderId());
throw ex;
}
}
}
@Service
public class AuditService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void record(String event, long orderId) {
auditRepository.save(event, orderId);
}
}这里有两个面试陷阱。第一,
auditService 必须是另一个经过代理的 Bean;如果把 record() 放回 OrderAppService,再用同类自调用,REQUIRES_NEW 也不会凭注解自动生效。第二,REQUIRES_NEW 创建的是独立的物理事务:外层事务会被挂起,内层可以独立提交或回滚,不是同一物理事务里的 savepoint。23独立事务不是免费开关。外层事务占着数据库资源时,内层还需要自己的资源;Spring 文档特别提醒,
REQUIRES_NEW 可能导致连接池耗尽,甚至在并发场景下形成死锁风险。3三种传播行为可以这样区分:
| 传播行为 | 面试时说清什么 | 适合怎么判断 |
|---|---|---|
REQUIRED | 没有外层就新建,有外层就加入;默认情况下共用同一个物理事务。3 | 主业务必须整体成功或整体失败时优先考虑。 |
REQUIRES_NEW | 暂停外层,创建独立物理事务;内外可以分别提交或回滚,但会额外申请资源。3 | 只有在「旁路记录必须独立保留」等语义明确时使用。 |
NESTED | 仍使用一个物理事务,通过 savepoint 做局部回滚;通常依赖 JDBC savepoint。3 | 不要把它和 REQUIRES_NEW 混称为嵌套新事务。 |
面试追问,按这个顺序排查
碰到「为什么没回滚」「为什么注解不生效」「为什么审计也没保存」这类题,不要一上来背传播属性。按 4 步说:
- 先看入口:调用者拿到的是代理,还是同一个对象里的
this调用?如果没经过代理,先别讨论传播行为。 - 再看物理事务:外层有没有事务?内层是默认
REQUIRED,还是明确要求REQUIRES_NEW/NESTED? - 再看异常:抛出的是 checked exception 还是
RuntimeException?异常有没有被 catch?是否配置了rollbackFor? - 最后看资源:独立事务是否需要第二个数据库连接?连接池容量和并发模型能不能承受?
你可以把答案组织成这样:
我先确认调用是否经过 Spring 代理。若是同类自调用,@Transactional和传播属性都可能不生效;若经过代理,再看当前是否已有物理事务。默认REQUIRED会加入外层,REQUIRES_NEW会创建独立事务。最后核对异常类型、回滚规则和连接资源,不能只看到注解就断言一定回滚。
4 个自测问题
- Controller 调用一个未标注事务的方法,而这个方法在同类中调用
@Transactional方法,事务到底从哪里开始? @Transactional方法抛出IOException,什么条件下数据库写入会回滚?- 内层
REQUIRED方法失败后被外层 catch,为什么外层提交时还可能抛UnexpectedRollbackException? - 审计必须独立保留时,为什么要拆成独立 Bean?
REQUIRES_NEW会带来什么连接池风险?
FAQ
@Transactional 写在 private 方法上可以吗?
面试里最稳的回答是:在默认 proxy 模式下,优先把事务标在可被代理外部调用的具体类方法上,通常使用
public 方法;接口代理还要求事务方法是接口中的 public 方法。不要用一个 private 方法上的注解证明事务一定会生效。1外层和内层都写 @Transactional,是不是就有两层独立事务?
我把异常 catch 住,再手动返回失败对象,事务会回滚吗?
不能只看返回对象。默认回滚规则看异常类型和拦截器最终观察到的异常;如果异常被吞掉,事务可能按正常返回处理。要么让应该触发回滚的异常继续抛出,要么显式配置回滚规则,或者把业务失败设计成明确的状态流转。
发布前自查清单
- 我能画出调用者、代理和目标对象之间的路径。
- 我能区分同类自调用与跨 Bean 调用。
- 我能说清
REQUIRED、REQUIRES_NEW、NESTED对物理事务的影响。 - 我不会把 checked exception 默认回滚当成 Spring 的规则。
- 我能解释
rollback-only为什么会让外层提交失败。 - 我能把独立事务和连接池资源放在同一个回答里。
面试官真正想听的不是「我用过
@Transactional」,而是你能把代理边界、物理事务、异常规则和资源代价连起来。答完这 4 个场景,再换成支付、库存或审计,判断顺序仍然成立。References
- 1Using @Transactional :: Spring Framework
docs.spring.io
- 2Proxying Mechanisms :: Spring Framework
docs.spring.io
- 3Transaction Propagation :: Spring Framework
docs.spring.io

求职面试实战指南
求职面试全方向深度攻略,参考面灵AI博客选题风格,覆盖面试技巧、技术考点、简历优化与AI工具评测。
This story was produced automatically by a channel. One sentence is all it takes for Neodrop to keep producing for you.
Related content
- Sign in to comment.