
聚焦 Spring `@Transactional` 面试中的自调用、异常回滚、传播行为和连接资源边界,给出 4 个代码场景与可直接复述的排查顺序。
@Transactional 默认是 PROPAGATION_REQUIRED:没有外层事务就新建,有外层事务就加入。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());
}
}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() 本身就是一次完整的下单事务,这种写法最直观。内部方法只是同一事务里的普通调用,不需要再依赖第二次代理拦截。@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());
}
}AopContext.currentProxy() 等处理方式,但把 AopContext.currentProxy() 标为不推荐的最后手段。面试回答里,先给「重构事务边界」或「拆分 Bean」通常比背 API 更稳。2IOException 是 checked exception。@Transactional
public void uploadAndSave(File file) throws IOException {
documentRepository.insert(file.name());
storageClient.upload(file); // 抛 IOException
}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等规则显式配置;最终要按业务原子性决定,而不是只按异常类名套模板。
@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,因为这个物理事务已经不能安全提交。3REQUIRES_NEW。先问清楚失败语义: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。23REQUIRES_NEW 可能导致连接池耗尽,甚至在并发场景下形成死锁风险。3| 传播行为 | 面试时说清什么 | 适合怎么判断 |
|---|---|---|
REQUIRED | 没有外层就新建,有外层就加入;默认情况下共用同一个物理事务。3 | 主业务必须整体成功或整体失败时优先考虑。 |
REQUIRES_NEW | 暂停外层,创建独立物理事务;内外可以分别提交或回滚,但会额外申请资源。3 | 只有在「旁路记录必须独立保留」等语义明确时使用。 |
NESTED | 仍使用一个物理事务,通过 savepoint 做局部回滚;通常依赖 JDBC savepoint。3 | 不要把它和 REQUIRES_NEW 混称为嵌套新事务。 |
this 调用?如果没经过代理,先别讨论传播行为。REQUIRED,还是明确要求 REQUIRES_NEW / NESTED?RuntimeException?异常有没有被 catch?是否配置了 rollbackFor?我先确认调用是否经过 Spring 代理。若是同类自调用,@Transactional和传播属性都可能不生效;若经过代理,再看当前是否已有物理事务。默认REQUIRED会加入外层,REQUIRES_NEW会创建独立事务。最后核对异常类型、回滚规则和连接资源,不能只看到注解就断言一定回滚。
@Transactional 方法,事务到底从哪里开始?@Transactional 方法抛出 IOException,什么条件下数据库写入会回滚?REQUIRED 方法失败后被外层 catch,为什么外层提交时还可能抛 UnexpectedRollbackException?REQUIRES_NEW 会带来什么连接池风险?@Transactional 写在 private 方法上可以吗?public 方法;接口代理还要求事务方法是接口中的 public 方法。不要用一个 private 方法上的注解证明事务一定会生效。1@Transactional,是不是就有两层独立事务?REQUIRED、REQUIRES_NEW、NESTED 对物理事务的影响。rollback-only 为什么会让外层提交失败。@Transactional」,而是你能把代理边界、物理事务、异常规则和资源代价连起来。答完这 4 个场景,再换成支付、库存或审计,判断顺序仍然成立。docs.spring.io
docs.spring.io
docs.spring.io
This story was produced automatically by a channel. One sentence is all it takes for Neodrop to keep producing for you.