摘要
Spring 事务失效是 Java 后端面试中的高频问题。面试官通常不只想听一句“同类自调用失效”,而是想确认你是否理解 Spring AOP 代理、异常回滚规则、事务边界、线程上下文和传播行为。本文按照“面试怎么问、应该怎么答、常见追问是什么”的方式,快速梳理 @Transactional 失效的 8 个典型场景。
前言
面试官问:
Spring 事务为什么会失效?
很多人的第一反应是:
同一个类里自调用,事务会失效。
这个答案没错,但只答这一句通常不够。
面试官真正想听的是:
-
你是否知道 Spring 事务依赖 AOP 代理;
-
你是否理解默认回滚规则;
-
你能不能分清事务失效和传播行为;
-
你有没有真正排查过事务问题。
这篇不从概念开始铺,直接从面试角度讲。
一、面试时先给结论
如果面试官刚问这个问题,可以先用 30 秒回答:
Spring 声明式事务主要通过 AOP 代理实现。
事务失效通常有三类原因:
第一,方法调用没有经过 Spring 代理,比如同类自调用、对象自己 new 出来;
第二,异常没有被事务拦截器感知,比如异常被 try-catch 吃掉,或者异常类型不符合默认回滚规则;
第三,数据库操作已经跑出原来的事务边界,比如开启了新线程、事务范围过小,或者传播行为配置不符合预期。
这段先把回答框架立住。
后面面试官继续追问,再展开具体场景。
二、场景一:同一个类中自调用
这是最常见的答案。
错误示例
@Service
public class OrderService {
public void createOrder() {
this.saveOrder();
}
@Transactional(rollbackFor = Exception.class)
public void saveOrder() {
// 插入订单
throw new RuntimeException("模拟异常");
}
}
问题出在:
this.saveOrder();
这是当前对象内部调用,没有经过 Spring 代理。
因此,saveOrder() 上的事务增强不会执行。
面试时怎么答
同类自调用事务失效,是因为 Spring 事务基于代理对象实现。
外部调用代理对象时,代理才能在方法执行前开启事务;
而 this.xxx() 调用的是当前对象本身,没有重新经过代理,所以事务注解不会被拦截。
推荐解决方式
将事务方法拆到另一个 Spring Bean:
@Service
public class OrderTransactionService {
@Transactional(rollbackFor = Exception.class)
public void saveOrder() {
// 插入订单
throw new RuntimeException("模拟异常");
}
}
@Service
public class OrderService {
private final OrderTransactionService transactionService;
public OrderService(OrderTransactionService transactionService) {
this.transactionService = transactionService;
}
public void createOrder() {
transactionService.saveOrder();
}
}
三、场景二:对象不是 Spring Bean
@Transactional 只有作用在 Spring 管理的对象上,才有机会创建代理。
错误示例
OrderService orderService = new OrderService();
orderService.saveOrder();
自己 new 出来的对象不是 Spring Bean,也没有代理对象。
此时 @Transactional 只是一个普通注解。
面试回答
如果对象是手动 new 出来的,没有交给 Spring 容器管理,就不会生成事务代理,所以事务不生效。
四、场景三:事务方法无法被正常代理
常见风险包括:
-
private方法; -
final方法; -
static方法; -
方法调用没有经过代理对象。
错误示例
@Transactional
private void saveOrder() {
// 数据库操作
}
或者:
@Transactional
public final void saveOrder() {
// 数据库操作
}
为了兼容不同代理方式、减少歧义,业务中的事务方法通常建议写成:
@Transactional(rollbackFor = Exception.class)
public void saveOrder() {
// 数据库操作
}
面试回答
Spring AOP 需要对方法进行代理增强。private 方法无法被外部代理调用,final 方法不能被重写,static 方法属于类本身,这些写法都可能导致事务增强无法正常执行。
注意,面试时不要只背:
非 public 方法一定失效。
更稳妥的表达是:
为了兼容不同代理模式,实际项目中事务方法通常建议使用 public,并确保调用经过 Spring 代理。
五、场景四:异常被 try-catch 吃掉
Spring 事务需要感知异常,才能决定是否回滚。
错误示例
@Transactional(rollbackFor = Exception.class)
public void saveOrder() {
orderRepository.insert();
try {
int result = 10 / 0;
} catch (Exception e) {
log.error("处理失败", e);
}
}
异常虽然发生了,但被捕获后没有继续抛出。
对于事务拦截器来说,这个方法是正常结束的,因此会提交事务。
正确写法
@Transactional(rollbackFor = Exception.class)
public void saveOrder() {
orderRepository.insert();
try {
int result = 10 / 0;
} catch (Exception e) {
log.error("处理失败", e);
throw e;
}
}
面试回答
异常被 catch 之后如果没有继续抛出,Spring 事务拦截器就感知不到异常,会认为方法正常执行完成,因此不会自动回滚。
六、场景五:受检异常没有配置 rollbackFor
Spring 默认对以下异常回滚:
-
RuntimeException -
Error
默认情况下,受检异常不会触发回滚。
示例
@Transactional
public void saveOrder() throws IOException {
orderRepository.insert();
throw new IOException("文件处理失败");
}
这个 IOException 默认不一定触发回滚。
推荐写法
@Transactional(rollbackFor = Exception.class)
public void saveOrder() throws IOException {
orderRepository.insert();
throw new IOException("文件处理失败");
}
面试回答
Spring 默认只对 RuntimeException 和 Error 回滚。
如果方法可能抛出 IOException 这类受检异常,需要通过 rollbackFor 显式指定回滚规则。
常见追问
面试官可能继续问:
是不是所有事务都要写 rollbackFor = Exception.class?
可以答:
不是强制要求。
如果业务只会抛运行时异常,默认规则就够用;
如果存在受检异常,或者项目希望统一回滚规则,可以显式配置 rollbackFor。
七、场景六:事务边界太小
有时候事务不是没生效,而是事务已经提前提交了。
示例
@Service
public class OrderTransactionService {
@Transactional
public void saveOrder() {
orderRepository.insert();
}
}
外层调用:
public void createOrder() {
orderTransactionService.saveOrder();
// 前面的事务已经提交
createOtherData();
throw new RuntimeException("后续业务失败");
}
异常发生时,订单事务已经提交,当然无法回滚。
正确思路
把完整业务单元放进一个事务边界:
@Transactional(rollbackFor = Exception.class)
public void createOrder() {
orderRepository.insert();
createOtherData();
throw new RuntimeException("模拟异常");
}
面试回答
事务应该覆盖一个完整的业务原子操作。
如果事务方法执行完已经提交,后面的业务再抛异常,就无法回滚前面已经提交的数据。
八、场景七:切换到新线程
Spring 常规事务上下文通常绑定在当前线程。
如果开启新线程:
@Transactional
public void createOrder() {
orderRepository.insert();
CompletableFuture.runAsync(() -> {
logRepository.insert();
throw new RuntimeException("异步任务失败");
});
}
异步线程和主线程不是同一个事务上下文。
子线程中的异常,也不会自动让主线程事务回滚。
面试回答
Spring 事务通常通过 ThreadLocal 绑定当前线程中的数据库连接和事务资源。
切换到新线程后,事务上下文不会自动传递,所以主线程事务无法覆盖异步线程中的数据库操作。
需要异步怎么办?
根据业务要求选择:
-
异步方法自己开启独立事务;
-
使用消息队列;
-
记录任务状态并重试;
-
使用补偿机制;
-
接受最终一致性。
不要误以为外层一个 @Transactional 能覆盖所有线程。
九、场景八:事务传播行为不符合预期
默认传播行为是:
Propagation.REQUIRED
它表示:
-
外层有事务,就加入外层事务;
-
外层没有事务,就创建新事务。
而:
Propagation.REQUIRES_NEW
表示挂起外层事务,创建一个独立事务。
示例
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveLog() {
logRepository.insert();
}
外层:
@Transactional
public void createOrder() {
orderRepository.insert();
logService.saveLog();
throw new RuntimeException("外层事务失败");
}
可能出现:
订单数据回滚
日志数据提交
因为日志使用的是独立事务。
这不是事务失效,而是传播行为本来如此。
面试回答
有些场景看起来像事务失效,其实是传播行为导致的。
例如 REQUIRES_NEW 会创建独立事务,内层事务提交后,外层事务回滚也不会影响它。
十、面试官最常追问的 5 个问题
1. Spring 事务为什么依赖代理?
因为代理对象需要在方法执行前后织入事务逻辑:
开启事务
执行目标方法
提交或回滚
没有经过代理,就没有这层事务增强。
2. 为什么同类自调用不生效?
因为:
this.saveOrder();
调用的是当前对象,不是 Spring 代理对象。
3. 为什么异常被捕获后不回滚?
因为异常没有继续传播到事务拦截器。
事务拦截器看到方法正常返回,就会提交。
4. 为什么异步线程不在同一个事务里?
因为事务资源一般绑定在当前线程,线程切换后不会自动携带原事务上下文。
5. REQUIRED 和 REQUIRES_NEW 有什么区别?
REQUIRED:
有事务就加入,没有就创建。
REQUIRES_NEW:
挂起当前事务,创建一个新的独立事务。
十一、事务失效排查顺序
实际面试中,如果被问:
线上发现事务没回滚,你怎么排查?
可以按这个顺序回答:
1. 确认当前对象是不是 Spring Bean;
2. 检查是不是自己 new 出来的对象;
3. 检查调用是否经过 Spring 代理;
4. 检查是否存在同类自调用;
5. 检查方法是否为 private、final、static;
6. 检查异常是否被 try-catch 吃掉;
7. 检查异常类型和 rollbackFor;
8. 检查是否切换了线程;
9. 检查事务传播行为;
10. 检查多数据源对应的事务管理器。
这个回答比单纯背 8 个场景更像有实际排查经验。
十二、面试完整回答模板
如果面试官问:
Spring 事务为什么会失效?
可以直接这样回答:
Spring 声明式事务主要基于 AOP 代理实现,所以事务失效通常和代理调用、异常传播以及事务边界有关。
常见情况有:
第一,同一个类中自调用事务方法,没有经过 Spring 代理;
第二,对象是手动 new 出来的,没有交给 Spring 容器管理;
第三,事务方法使用 private、final 或 static 等无法正常增强的方法;
第四,异常被 try-catch 捕获后没有继续抛出,事务拦截器感知不到异常;
第五,抛出的是受检异常,而默认规则只对 RuntimeException 和 Error 回滚,需要配置 rollbackFor;
第六,事务范围设置得太小,异常发生时前面的事务已经提交;
第七,在异步线程或新线程中执行数据库操作,事务上下文不会自动跨线程传播;
第八,事务传播行为配置不符合预期,比如 REQUIRES_NEW 创建了独立事务。
实际排查时,我会先确认对象是否由 Spring 管理,再检查调用是否经过代理,然后检查异常传播、回滚规则、线程切换、传播行为以及事务管理器是否正确。
这段回答有原理、有场景,也有排查思路,基本够用了。
十三、一句话总结
Spring 事务面试题,重点不是背一句:
同类自调用会失效。
而是要围绕三个问题展开:
调用有没有经过 Spring 代理?
异常有没有被事务拦截器感知?
数据库操作是否在预期的事务边界内?
把这三件事讲清楚,Spring 事务失效的问题基本就不会答乱。
大家加油:)

403

被折叠的 条评论
为什么被折叠?



