Spring事务为什么会失效?常见场景与解决方案总结

Spring事务为什么会失效?常见场景与解决方案总结

在 Spring 开发中,很多开发者认为:

java 复制代码
@Transactional

加上事务注解后,事务一定会生效。

但实际开发中经常出现:

  • 数据已经提交,事务却没有回滚
  • 方法明明加了 @Transactional 却没有开启事务
  • 多线程场景事务失效
  • try-catch 后事务不回滚

本文结合 Spring 源码原理,总结事务失效的常见场景以及解决方案。


目录

  • Spring事务简介
  • Spring事务实现原理
  • Transactional执行流程
  • ThreadLocal与事务
  • 事务失效的常见场景
    • 非public方法
    • 同类内部调用
    • 异常被捕获
    • 抛出受检异常
    • 多线程事务失效
    • 手动new对象
    • 数据库引擎不支持事务
  • 多线程事务失效深度解析
  • 解决方案
  • 面试高频问题
  • 总结

一、Spring事务简介

Spring事务的核心注解:

java 复制代码
@Transactional

示例:

java 复制代码
@Service
public class UserService {

    @Transactional
    public void createUser() {

        userMapper.insert(user);

        orderMapper.insert(order);
    }
}

作用:

text 复制代码
保证多个数据库操作
要么全部成功
要么全部失败

二、Spring事务实现原理

很多人以为:

text 复制代码
@Transactional
=
数据库事务

实际上:

text 复制代码
@Transactional
=
AOP + 数据库事务

Spring会为目标对象创建代理对象:

text 复制代码
UserService

↓

TransactionProxy

↓

真实UserService

调用流程:

text 复制代码
调用代理对象

↓

开启事务

↓

执行目标方法

↓

提交事务

或

回滚事务

三、Transactional执行流程

例如:

java 复制代码
@Transactional
public void save() {

}

执行流程:

text 复制代码
进入代理对象

↓

begin transaction

↓

执行save()

↓

commit

↓

结束

如果发生异常:

text 复制代码
begin transaction

↓

执行save()

↓

exception

↓

rollback

四、ThreadLocal与事务

Spring事务核心依赖:

java 复制代码
ThreadLocal

源码中:

java 复制代码
TransactionSynchronizationManager

内部维护:

java 复制代码
private static final ThreadLocal<Map<Object,Object>>

作用:

text 复制代码
保存当前线程事务上下文

例如:

text 复制代码
线程A

↓

数据库连接A

↓

事务A

text 复制代码
线程B

↓

数据库连接B

↓

事务B

因此:

text 复制代码
事务和线程绑定

这是后面多线程事务失效的根本原因。


五、事务失效场景一:非public方法

错误示例:

java 复制代码
@Transactional
private void saveUser() {

}

原因:

Spring默认基于代理实现事务。

代理只能拦截:

java 复制代码
public

方法。


正确写法:

java 复制代码
@Transactional
public void saveUser() {

}

六、事务失效场景二:同类内部调用

这是最经典面试题。


示例:

java 复制代码
@Service
public class UserService {

    public void test() {

        saveUser();
    }

    @Transactional
    public void saveUser() {

    }
}

问题:

java 复制代码
test()

调用:

java 复制代码
saveUser()

事务失效。


为什么?

正常事务流程:

text 复制代码
代理对象

↓

saveUser()

↓

事务生效

内部调用:

text 复制代码
this.saveUser()

绕过代理。

直接调用目标对象。


结果:

text 复制代码
事务失效

解决方案:

通过代理调用:

java 复制代码
@Autowired
private UserService userService;

public void test() {

    userService.saveUser();
}

七、事务失效场景三:异常被捕获

错误示例:

java 复制代码
@Transactional
public void save() {

    try {

        int a = 1 / 0;

    } catch (Exception e) {

        log.error("error");
    }
}

结果:

text 复制代码
事务提交

不会回滚。


原因:

Spring判断:

text 复制代码
方法是否抛出异常

这里:

java 复制代码
catch

已经吞掉异常。

Spring感知不到异常。


解决:

继续抛出:

java 复制代码
catch (Exception e) {

    throw e;
}

或者:

java 复制代码
TransactionAspectSupport
.currentTransactionStatus()
.setRollbackOnly();

八、事务失效场景四:受检异常

默认情况下:

java 复制代码
@Transactional

只会回滚:

java 复制代码
RuntimeException

Error

例如:

java 复制代码
throw new RuntimeException();

事务回滚。


但是:

java 复制代码
throw new IOException();

事务不会回滚。


解决方案:

java 复制代码
@Transactional(
    rollbackFor = Exception.class
)

推荐统一写法:

java 复制代码
@Transactional(
    rollbackFor = Exception.class
)

九、事务失效场景五:多线程事务失效

这是实际项目最容易踩坑的问题。


示例:

java 复制代码
@Transactional
public void save() {

    userMapper.insert(user);

    new Thread(() -> {

        orderMapper.insert(order);

    }).start();

    int a = 1 / 0;
}

开发者期望:

text 复制代码
异常发生

↓

全部回滚

实际结果:

text 复制代码
user回滚

order提交

为什么?

因为:

text 复制代码
事务基于ThreadLocal

主线程:

text 复制代码
Thread-A

↓

事务A

新线程:

text 复制代码
Thread-B

↓

没有事务

因此:

java 复制代码
orderMapper.insert()

直接提交。


十、事务失效场景六:手动new对象

错误示例:

java 复制代码
UserService service =
        new UserService();

调用:

java 复制代码
service.save();

原因:

text 复制代码
Spring没有管理该对象

没有代理对象。

事务无法生效。


正确写法:

java 复制代码
@Autowired
private UserService userService;

十一、事务失效场景七:数据库不支持事务

例如:

MySQL:

text 复制代码
MyISAM

引擎。


查看:

sql 复制代码
SHOW TABLE STATUS;

如果:

text 复制代码
Engine=MyISAM

事务无效。


推荐:

text 复制代码
InnoDB

十二、多线程事务失效深度解析

这是面试中的高频问题。


代码:

java 复制代码
@Transactional
public void saveOrder() {

}

Spring执行:

text 复制代码
ThreadLocal

↓

Connection

↓

Transaction

绑定。


如果开启新线程:

java 复制代码
CompletableFuture.runAsync(...)

或者:

java 复制代码
new Thread(...)

线程切换后:

text 复制代码
ThreadLocal丢失

新线程获取不到:

text 复制代码
事务上下文

最终:

text 复制代码
事务失效

十三、多线程事务解决方案

方案一:避免事务中开启线程

推荐:

text 复制代码
事务负责事务

线程负责线程

解耦。


方案二:消息队列

例如:

text 复制代码
RabbitMQ

Kafka

RocketMQ

主事务提交后:

text 复制代码
发送消息

异步线程消费。


方案三:子线程独立事务

例如:

java 复制代码
@Transactional(
    propagation =
    Propagation.REQUIRES_NEW
)

每个线程独立事务。


方案四:编程式事务

java 复制代码
TransactionTemplate

手动控制事务。


十四、事务传播行为

常见传播行为:

传播级别 说明
REQUIRED 默认,有事务加入,无事务创建
REQUIRES_NEW 创建新事务
SUPPORTS 支持事务
NOT_SUPPORTED 不支持事务
MANDATORY 必须存在事务
NEVER 必须不存在事务
NESTED 嵌套事务

最常用:

java 复制代码
REQUIRED

和:

java 复制代码
REQUIRES_NEW

十五、面试高频问题

面试题1

Spring事务实现原理?

答案:

text 复制代码
AOP + 数据库事务

面试题2

为什么同类调用事务失效?

答案:

text 复制代码
绕过代理对象

面试题3

为什么try-catch导致事务失效?

答案:

text 复制代码
异常被吞掉

Spring感知不到异常

面试题4

为什么多线程事务失效?

答案:

text 复制代码
事务基于ThreadLocal

线程切换后

获取不到事务上下文

面试题5

@Transactional默认回滚哪些异常?

答案:

text 复制代码
RuntimeException

Error

面试题6

如何让所有异常回滚?

答案:

java 复制代码
@Transactional(
    rollbackFor = Exception.class
)

十六、开发最佳实践

统一写法:

java 复制代码
@Transactional(
    rollbackFor = Exception.class
)

避免:

java 复制代码
new Thread()

出现在事务中。


避免:

java 复制代码
this.xxx()

调用事务方法。


避免:

java 复制代码
try-catch

吞掉异常。


优先使用:

text 复制代码
消息队列

异步任务

事件驱动

处理事务外逻辑。


总结

Spring事务本质:

text 复制代码
AOP + ThreadLocal + 数据库事务

事务失效高频场景:

  1. 非public方法
  2. 同类内部调用
  3. 异常被捕获
  4. 抛出受检异常
  5. 多线程调用
  6. 手动new对象
  7. 数据库不支持事务

其中面试最常问的是:

text 复制代码
同类调用事务失效

和:

text 复制代码
多线程事务失效

牢记一句话:

Spring事务与当前线程绑定,事务上下文存储在ThreadLocal中。线程切换后无法共享事务上下文,因此多线程场景下事务天然失效。


相关推荐
cfm_29141 小时前
JVM对象逃逸分析深度详解
java·开发语言·jvm
云絮.1 小时前
数据库约束
java·数据库·sql·mysql·oracle
weixin_523185321 小时前
SimpleDateFormat为什么线程不安全?源码级解析与解决方案
java·开发语言·安全
Chase_______1 小时前
【Java杂项】Java 中的 null:空指针、自动拆箱与集合边界详解
java·开发语言
程序猿乐锅1 小时前
【JAVASE | 第十九篇】Java 注解入门
java
布朗克1681 小时前
28 网络编程——Socket、TCP/UDP与HttpClient
java·网络·tcp/ip·udp
二月夜9 小时前
剖析Java正则表达式回溯问题
java·正则表达式
cui_ruicheng9 小时前
MySQL(四):数据类型与字段设计
数据库·mysql
xuhaoyu_cpp_java10 小时前
项目学习(三)分页查询
java·经验分享·笔记·学习