Spring 事务失效

xiaopeng
发布于 2026-08-18 / 4 阅读
0
0

Spring 事务失效

Spring 事务失效 ⭐⭐⭐⭐⭐

面试频率:★★★★★
工作频率:★★★★★


⚡30 秒速记

🟢 一句话

Spring 声明式事务主要依赖 AOP 代理。判断事务为什么失效,首先看方法调用有没有经过 Spring 事务代理,其次看异常是否满足回滚规则。

常见失效场景:

1. 同类内部调用
2. private 方法
3. 对象不是 Spring Bean
4. 异常被 catch 吃掉
5. 异常不满足回滚规则
6. 底层资源不支持事务
7. 多线程 / @Async 导致事务上下文不在同一线程

核心判断:

@Transactional
      ↓
对象是不是Spring Bean?
      ↓
调用有没有经过代理?
      ↓
事务拦截器有没有执行?
      ↓
异常有没有抛出去?
      ↓
异常是否满足回滚规则?
      ↓
底层资源是否支持事务?

一、@Transactional 为什么能实现事务? ⭐⭐⭐⭐⭐

例如:

@Transactional
public void save() {

    insertOrder();

}

不是因为:

@Transactional 注解自己会开启事务

而是 Spring 通过:

AOP
+
动态代理

实现。

大致流程:

调用者
   ↓
Spring代理对象
   ↓
事务拦截器
   ↓
开启事务
   ↓
调用目标方法
   ↓
是否发生需要回滚的异常?
   ↙              ↘
没有              有
 ↓                 ↓
commit           rollback

所以:

@Transactional 能不能生效,和 Spring 代理有很大关系。


二、失效场景1:同类内部调用 ⭐⭐⭐⭐⭐

这是最经典的事务失效场景之一。

例如:

@Service
public class OrderService {

    public void createOrder() {

        save();

    }


    @Transactional
    public void save() {

        // 保存订单

    }

}

看起来:

createOrder()

↓

save()

↓

@Transactional

↓

开启事务

实际上:

save();

相当于:

this.save();

属于:

同一个对象内部调用。


正常事务调用:

Controller
    ↓
OrderService代理对象
    ↓
事务拦截器
    ↓
save()
    ↓
原始OrderService

但是自调用:

Controller
    ↓
OrderService代理对象
    ↓
createOrder()
    ↓
原始OrderService
    ↓
this.save()

注意:

this.save()

没有重新经过:

OrderService代理对象

所以:

事务拦截器没有机会拦截save()

导致:

@Transactional

可能不生效。


三、怎么解决同类调用?

比较常见的方式:

把事务方法拆到另外一个 Spring Bean。

例如:

@Service
public class OrderTransactionService {

    @Transactional
    public void save() {

        // 保存订单

    }

}

然后:

@Service
public class OrderService {

    @Autowired
    private OrderTransactionService transactionService;


    public void createOrder() {

        transactionService.save();

    }

}

执行:

OrderService
    ↓
OrderTransactionService代理对象
    ↓
事务拦截器
    ↓
开启事务
    ↓
save()

这样:

调用经过 Spring 代理。

事务可以正常生效。


四、失效场景2:private 方法 ⭐⭐⭐⭐⭐

例如:

@Transactional
private void save() {

}

这种写法:

不要指望普通 Spring 声明式事务代理按预期生效。

因为:

@Transactional

↓

依赖AOP代理拦截方法

↓

private方法不能像普通可代理方法一样被代理增强

尤其 CGLIB:

通过:

继承目标类

↓

生成代理子类

↓

重写方法

实现代理。

但是:

private方法

子类不能重写。

所以:

无法像普通 public 方法一样进行代理增强。


五、失效场景3:自己 new 对象 ⭐⭐⭐⭐⭐

例如:

OrderService orderService =
        new OrderService();

orderService.save();

即使:

@Transactional
public void save() {

}

也不会因为这个注解自动产生 Spring 事务。

原因:

new OrderService()
      ↓
普通Java对象
      ↓
不是Spring容器管理的代理Bean
      ↓
没有事务拦截器

所以:

自己new的对象

≠

Spring容器里的Bean / 代理对象

正常情况:

@Service
public class OrderService {

}

然后让 Spring 注入:

@Autowired
private OrderService orderService;

此时:

Spring容器
   ↓
OrderService Bean
   ↓
可能经过AOP代理
   ↓
@Transactional生效

六、失效场景4:异常被 catch 吃掉 ⭐⭐⭐⭐⭐

这个工作中非常容易踩坑。

例如:

@Transactional
public void createOrder() {

    try {

        saveOrder();

        int i = 1 / 0;

    } catch (Exception e) {

        log.error("创建订单失败", e);

    }

}

执行:

开启事务

↓

saveOrder()

↓

发生异常

↓

catch捕获

↓

没有继续抛出

↓

方法正常结束

事务拦截器看到:

方法正常返回

可能就会:

commit

而不是:

rollback

七、为什么 catch 会影响回滚?

事务代理大致:

开启事务

↓

try {

    调用业务方法

    commit

} catch {

    rollback

}

但是业务方法内部:

try {

    // 异常

} catch (Exception e) {

    // 吃掉异常

}

导致代理层:

根本没有收到异常

所以:

无法根据这个异常触发预期回滚。


八、怎么解决异常被吃掉?

如果业务要求:

发生异常

↓

整个事务回滚

可以:

记录日志以后继续抛出:

@Transactional
public void createOrder() {

    try {

        saveOrder();

    } catch (Exception e) {

        log.error("创建订单失败", e);

        throw e;

    }

}

流程:

业务发生异常

↓

catch

↓

记录日志

↓

重新throw

↓

异常传到事务代理

↓

rollback

九、失效场景5:异常不满足回滚规则 ⭐⭐⭐⭐⭐

很多人认为:

@Transactional

↓

任何Exception

↓

全部回滚

这是不准确的。

Spring 默认情况下主要对:

RuntimeException

Error

进行回滚。

例如:

@Transactional
public void save() throws Exception {

    throw new Exception("失败");

}

这里:

Exception

属于 checked exception。

默认规则下:

不一定按照你想象的方式回滚。


十、rollbackFor ⭐⭐⭐⭐⭐

实际项目非常常见:

@Transactional(
    rollbackFor = Exception.class
)
public void save() throws Exception {

}

意思:

Exception及其子类

↓

发生异常

↓

按照配置进行回滚

所以很多项目统一写:

@Transactional(
    rollbackFor = Exception.class
)

避免 checked exception 没有触发预期回滚。


十一、RuntimeException 和 Exception

简单记:

Exception
    │
    ├── RuntimeException
    │
    └── 其他Checked Exception

Spring 默认:

RuntimeException

↓

回滚
Error

↓

回滚

而普通 checked exception:

默认不回滚

如果需要:

rollbackFor = Exception.class

十二、失效场景6:底层资源不支持事务

Spring:

@Transactional

只是负责:

事务管理

最终:

真正的数据提交和回滚:

还需要底层资源支持事务。

例如数据库存储引擎:

支持事务

↓

rollback才有意义

如果:

底层资源本身不支持事务

那么:

Spring想rollback

↓

底层无法真正回滚

十三、失效场景7:多线程 ⭐⭐⭐⭐⭐

这个也非常重要。

Spring 数据库事务通常:

和当前线程绑定。

例如:

@Transactional
public void createOrder() {

    saveOrder();

    new Thread(() -> {

        updateStock();

    }).start();

}

执行:

主线程
  ↓
事务A
  ↓
saveOrder()

但是:

新线程
  ↓
updateStock()

新线程:

不会自动继承主线程的数据库事务上下文。

所以:

主线程事务A

≠

新线程事务

十四、@Async 和事务

例如:

@Transactional
public void createOrder() {

    asyncService.updateStock();

}

而:

@Async
public void updateStock() {

}

updateStock()

通常会在线程池的另一个线程执行。

所以:

createOrder

↓

线程A

↓

事务A

而:

@Async updateStock

↓

线程B

线程B:

不会自动加入线程A的数据库事务。

所以不要想当然认为:

@Transactional方法

↓

里面所有异步代码

↓

都是同一个事务

不是。


十五、为什么事务和线程有关?

Spring 常见事务管理会把:

数据库连接

事务状态

事务资源

和:

当前线程

关联起来。

可以粗略理解:

Thread A

↓

Connection A

↓

Transaction A

换线程:

Thread B

↓

没有Thread A的事务上下文

所以:

事务不会自动跨线程传播

十六、和事务传播机制不要搞混 ⭐⭐⭐⭐⭐

你刚学过:

REQUIRED

REQUIRES_NEW

例如:

A方法
 ↓
B方法

如果:

同一个线程

+

调用经过Spring代理

事务传播机制才会决定:

B加入A事务?

还是

B创建新事务?

但是:

如果:

A在线程1

B在线程2

那就不是简单的:

REQUIRED加入事务A

了。

因为:

事务上下文通常没有跨线程过去。


十七、事务失效统一理解 ⭐⭐⭐⭐⭐

不要死背:

10种事务失效场景

以后看到事务问题:

按照下面顺序排查。


第一步:是不是 Spring Bean?

对象是不是Spring创建的?

↓

自己new的?

↓

如果自己new → 没有Spring代理

第二步:有没有经过代理?

外部调用?

还是

this.xxx()?

如果:

this.xxx()

要警惕:

自调用绕过代理

第三步:方法能不能正常被代理?

例如:

private
final(特别是基于子类代理时)

要考虑:

代理限制。


第四步:异常有没有抛出去?

发生异常

↓

是不是catch掉了?

↓

有没有重新throw?

第五步:异常满足回滚规则吗?

RuntimeException?

Error?

Checked Exception?

rollbackFor配置了吗?

第六步:是不是换线程了?

new Thread

@Async

线程池

事务上下文:

通常不会自动跨线程传播

第七步:底层支持事务吗?

例如:

数据库 / 存储引擎

是否真正支持:

commit

rollback

十八、事务失效排查图 ⭐⭐⭐⭐⭐

@Transactional不生效
        ↓
是不是Spring Bean?
   ↓            ↓
  否            是
  ↓             ↓
没有代理      是否经过代理?
                ↓
         ┌──────┴──────┐
         ↓             ↓
       没有            有
         ↓             ↓
    检查自调用       方法是否可代理?
                       ↓
                 异常是否抛出?
                       ↓
                 是否满足回滚规则?
                       ↓
                  是否切换线程?
                       ↓
                 底层是否支持事务?

十九、和之前知识串起来 ⭐⭐⭐⭐⭐

现在:

@Transactional
      ↓
Spring AOP
      ↓
动态代理
      ↓
事务拦截器
      ↓
事务传播机制
      ↓
数据库事务

所以:

Spring AOP

学懂以后:

事务失效很多问题都不需要死背。

例如:

this.save()

↓

绕过代理

↓

@Transactional失效

以及:

REQUIRES_NEW

↓

需要事务代理识别传播行为

↓

this.save()

↓

绕过代理

↓

REQUIRES_NEW可能无法按预期生效

二十、几个经典面试代码题 ⭐⭐⭐⭐⭐

题目1

public void A() {

    B();

}

@Transactional
public void B() {

}

问:

B有事务吗?

答:

如果属于同一个 Bean 的内部调用,B 的 @Transactional 可能因为自调用绕过代理而不生效。


题目2

@Transactional
public void A() {

    try {

        insert();

        throw new RuntimeException();

    } catch (Exception e) {

    }

}

问:

会回滚吗?

答:

异常被内部 catch 后没有继续抛出,事务代理可能认为方法正常结束,因此不会因为这个异常按预期触发回滚。


题目3

@Transactional
public void A() throws Exception {

    insert();

    throw new Exception();

}

问:

默认一定回滚吗?

答:

不一定。普通 checked exception 默认不属于 Spring 声明式事务的回滚范围,可以使用 rollbackFor = Exception.class 指定。


题目4

@Transactional
public void A() {

    new Thread(() -> {

        update();

    }).start();

}

问:

update 和 A 是同一个事务吗?

答:

通常不是。Spring 事务上下文通常绑定在线程上,新线程不会自动继承原线程事务。


🎤 面试回答(30秒)

Spring 声明式事务主要基于 AOP 代理实现,所以事务失效首先要考虑方法调用是否经过 Spring 事务代理。

常见场景包括同类内部自调用绕过代理、private 等方法无法按预期代理、对象自己 new 没有交给 Spring 管理、异常被 catch 后没有继续抛出、异常不满足默认回滚规则,以及多线程或 @Async 导致事务上下文不在同一个线程。

排查事务问题时,我一般会从 Spring Bean、代理调用、异常传播、回滚规则和线程这几个方面检查。


🎯 面试追问

Q1:Spring事务为什么会失效?

答:

核心原因通常从:

代理

异常

线程

底层事务支持

几个方面排查。


Q2:为什么同类内部调用事务可能失效?

答:

因为:

this.save();

没有重新经过 Spring 代理对象。

所以:

事务拦截器无法拦截这个方法调用。


Q3:为什么 private 方法不建议加 @Transactional?

答:

因为 Spring 声明式事务依赖 AOP 代理,private 方法不能像普通可代理方法一样被代理增强。


Q4:为什么自己 new 的对象事务不生效?

答:

因为:

new OrderService()

只是普通 Java 对象。

不是:

Spring容器提供的代理Bean

所以:

没有事务拦截器。


Q5:为什么 catch 异常可能导致事务不回滚?

答:

因为:

异常被业务代码吃掉

↓

没有传播到事务代理

↓

代理认为方法正常结束

所以不会因为这个异常按预期触发回滚。


Q6:Spring 默认什么异常回滚?

答:

默认主要对:

RuntimeException

Error

进行回滚。

普通 checked exception:

默认不回滚。


Q7:rollbackFor = Exception.class 有什么作用?

答:

@Transactional(
    rollbackFor = Exception.class
)

表示:

Exception 及其子类都按照该配置参与事务回滚判断。


Q8:为什么 @Async 可能导致事务问题?

答:

因为:

@Async

↓

切换线程

而 Spring 数据库事务上下文通常:

绑定当前线程

所以异步线程不会自动加入调用线程已有的事务。


Q9:事务传播和多线程是一回事吗?

答:

不是。

事务传播主要解决:

事务方法A

↓

调用事务方法B

↓

B如何处理当前事务

通常是在同线程调用链中讨论。

多线程:

线程A

↓

线程B

事务上下文通常不会自动跨线程传播。


⚠️ 工作踩坑

1. 看到 @Transactional 就认为一定生效

错误。

应该检查:

是不是Spring Bean?

↓

有没有经过代理?

↓

有没有自调用?

↓

异常有没有抛出?

↓

异常是否满足回滚规则?

↓

有没有切换线程?

2. catch 以后什么都不做

例如:

try {

    save();

} catch (Exception e) {

    log.error("error", e);

}

如果业务要求整个事务回滚:

不要把异常吃掉后直接正常返回。


3. REQUIRES_NEW 同类调用

例如:

@Transactional
public void A() {

    B();

}

@Transactional(
    propagation = Propagation.REQUIRES_NEW
)
public void B() {

}

如果:

A和B在同一个Bean

那么:

B()

↓

this.B()

↓

绕过代理

REQUIRES_NEW 可能不会按预期生效。

比较常见的处理:

把B拆到另一个Spring Bean

4. @Async 里认为还能共享外层事务

错误。

线程A:事务A

@Async

↓

线程B

线程B:

通常不会自动共享线程A的事务。


❓ 自测

  1. @Transactional 底层为什么依赖 AOP?

  2. 判断事务失效首先应该想到什么?

  3. 为什么同类内部调用可能导致事务失效?

  4. this.save() 为什么会绕过代理?

  5. private 方法为什么不适合普通 Spring 声明式事务?

  6. 自己 new 的 Service 为什么事务不生效?

  7. 为什么异常被 catch 后可能不回滚?

  8. catch 后重新 throw 为什么可以触发事务代理的回滚判断?

  9. Spring 默认哪些异常会回滚?

  10. checked exception 默认一定回滚吗?

  11. rollbackFor = Exception.class 有什么作用?

  12. 为什么 @Async / new Thread 会影响事务?

  13. Spring 数据库事务为什么和线程有关?

  14. REQUIRES_NEW 为什么也可能因为自调用失效?

  15. 事务传播和多线程事务有什么区别?

  16. 排查 @Transactional 不生效,你会从哪些方面检查?


评论