大学生学习记录,有不足的地方,欢迎指出,感谢大家。
Spring 事务原子性问题排查与修复
Situation(背景)
在 local-task-message 项目中,有一个业务操作需要同时写入两张表:
- 业务表 (
order_list)--- 通过 MyBatis 插入 (是在测试项目中) - 本地消息表 (
local_task_message)--- 通过原生 JDBC 插入(TaskMessageDaoImpl)(这里本地消息表的存储是调用了我写的一个组件项目local-task-message,为了实现兼容,使用的是原生JDBC,然后获取数据库连接用的是dataSource.getConnection())
两者需要保证事务原子性:要么都成功,要么都回滚。
项目结构:
local-task-message:核心库模块,包含TaskMessageDaoImpl(原生 JDBC)local-task-message-test:测试模块,包含业务表的 MyBatis DAO 和测试代码
Target(目标)
排查并修复以下问题:
- 测试中业务表插入失败,但本地消息表已提交(数据不一致,明明写了@Transactional但是两个表不在同一事务)
- 两种场景(AOP 事务 / @Transactional 测试事务)下原子性均无法保证
Action(行动)
问题定位
根因 :TaskMessageDaoImpl 使用 dataSource.getConnection() 直接从 HikariCP 连接池获取连接,绕过了 Spring 事务管理。
less
事务链路对比:
错误链路:
@Transactional 开启事务
→ DataSourceTransactionManager 获取连接A,绑定到 TransactionSynchronizationManager
→ MyBatis 操作 → 用连接A(事务内) ✓
→ TaskMessageDaoImpl.insert()
→ dataSource.getConnection() → 从 HikariCP 拿到连接B(新连接,autoCommit=true)→ 独立提交 ✗
正确链路:
@Transactional 开启事务
→ DataSourceTransactionManager 获取连接A,绑定到 TransactionSynchronizationManager
→ MyBatis 操作 → 用连接A(事务内) ✓
→ TaskMessageDaoImpl.insert()
→ DataSourceUtils.getConnection() → 检查 TransactionSynchronizationManager → 返回连接A(同一个事务) ✓
关键知识点
dataSource.getConnection() vs DataSourceUtils.getConnection(dataSource):
| 方式 | 行为 | 是否参与 Spring 事务 |
|---|---|---|
dataSource.getConnection() |
直接从连接池获取新连接 | 否,autoCommit=true,独立提交 |
DataSourceUtils.getConnection(dataSource) |
先检查 TransactionSynchronizationManager 是否有绑定连接 |
是,返回事务内的同一个连接 |
Spring 事务绑定机制:
@Transactional→DataSourceTransactionManager.doBegin()- 内部调用
DataSourceUtils.getConnection()获取连接 - 关闭
autoCommit,绑定到TransactionSynchronizationManager(ThreadLocal) - 后续同一事务内的
DataSourceUtils.getConnection()返回同一个连接 - 事务提交/回滚后,连接释放回连接池
修复内容
-
TaskMessageDaoImpl.java(核心库)- 添加
import org.springframework.jdbc.datasource.DataSourceUtils; - 所有
dataSource.getConnection()→DataSourceUtils.getConnection(dataSource)
- 添加
-
local-task-message/pom.xml(核心库)- 添加
spring-jdbc依赖(DataSourceUtils所在包)
- 添加
-
application-dev.yml(测试模块)- 添加 HikariCP 连接池参数(
max-lifetime、keepalive-time等),防止调试时连接超时
- 添加 HikariCP 连接池参数(
Result(结果)
修复后事务链路完整:
- MyBatis 和原生 JDBC 共用同一个数据库连接
- 同一事务内,要么全部提交,要么全部回滚
- 调试断点场景下连接保活,不会因空闲超时失效
延伸知识
1. Spring Test 的 @Transactional 默认回滚
Spring Test 中,测试方法上的 @Transactional 默认在测试结束后自动回滚 ,不会真正写入数据库。这是为了保持测试环境干净。如需提交,可加 @Rollback(false)。
2. @Transactional 自调用失效
同类内方法 A 调用方法 B(B 上有 @Transactional),事务不生效。因为 Spring AOP 基于代理,自调用绕过了代理对象。解决方式:通过注入的 Bean 调用,或将 @Transactional 放到外层方法上。
3. HikariCP 连接池参数(调试场景)
| 参数 | 建议值 | 作用 |
|---|---|---|
max-lifetime |
1800000(30分钟) | 连接最大存活时间,应小于 MySQL wait_timeout |
idle-timeout |
600000(10分钟) | 空闲连接回收时间 |
keepalive-time |
300000(5分钟) | 定期心跳保活,防止调试断点时连接被 MySQL 断开 |
validation-timeout |
5000(5秒) | 借出连接前校验有效性 |
4. 原生 JDBC 与 Spring 事务集成的正确姿势
ini
// ❌ 错误:绕过 Spring 事务
Connection connection = dataSource.getConnection();
// ✅ 正确:参与 Spring 事务
Connection connection = DataSourceUtils.getConnection(dataSource);
依赖:spring-jdbc(提供 DataSourceUtils)