Spring 事务原子性问题排查与修复

大学生学习记录,有不足的地方,欢迎指出,感谢大家。

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(目标)

排查并修复以下问题:

  1. 测试中业务表插入失败,但本地消息表已提交(数据不一致,明明写了@Transactional但是两个表不在同一事务)
  2. 两种场景(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 事务绑定机制:

  1. @Transactional → DataSourceTransactionManager.doBegin()
  2. 内部调用 DataSourceUtils.getConnection() 获取连接
  3. 关闭 autoCommit,绑定到 TransactionSynchronizationManager(ThreadLocal)
  4. 后续同一事务内的 DataSourceUtils.getConnection() 返回同一个连接
  5. 事务提交/回滚后,连接释放回连接池

修复内容

  1. TaskMessageDaoImpl.java(核心库)

    • 添加 import org.springframework.jdbc.datasource.DataSourceUtils;
    • 所有 dataSource.getConnection() → DataSourceUtils.getConnection(dataSource)
  2. local-task-message/pom.xml(核心库)

    • 添加 spring-jdbc 依赖(DataSourceUtils 所在包)
  3. application-dev.yml(测试模块)

    • 添加 HikariCP 连接池参数(max-lifetime、keepalive-time 等),防止调试时连接超时

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)

相关推荐
高频因子挖掘机1 小时前
实时行情为什么不应该逐只股票请求?批量查询与标的池管理实践
后端·github·api
程序猿DD6 小时前
OctaFuse Gateway 2.15.0:MCP 聚合、向量 Embeddings 与官方目录同步
后端·agent
余槐i8 小时前
PostgreSQL 函数索引报错 must be immutable:3种绕法与代价
数据库·后端·sql·postgresql·性能优化
需要8269 小时前
Spring Bean 生命周期:从实例化到销毁的完整钩子
java·后端·spring
凤山老林10 小时前
Spring Boot 接入国密 SM2/SM3/SM4 的一些实践记录
spring boot·后端·python
伯恩bourne11 小时前
Spring Boot Bean 注册的两种方式:@Component 家族 vs @Configuration + @Bean
java·spring boot·后端
张彦峰ZYF11 小时前
停顿短不等于延迟低:G1、ZGC 与低延迟服务的目标分野、代价账本与选型标定
后端·回收暂停、应用停止总时长·到达安全点、分配停滞·屏障吃吞吐、并发线程吃核心·并发窗口吃堆余量·分区式靠挑回收集兑现目标·染色指针式不支持压缩引用
pride.li12 小时前
第03章 Python 教程-基础语法:注释与变量
服务器·前端·后端·python·算法·计算机视觉
upala12 小时前
【仓颉语言入门 · 第二篇 第 6 课】
后端
罗丹丹Luna12 小时前
移动端显示“已提交”以后:SAP 与低代码集成该怎样暴露真实状态
后端·架构