Java 面试:@Transactional 为什么会失效?面试官真正想听的 8 个场景

摘要

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 事务失效的问题基本就不会答乱。

大家加油:)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值