异步任务中 Hibernate 获取当前 Session 失败:Could not obtain transaction-synchronized Session for current thread

原创 临窗旋墨 2026-09-23 阅读:16 分类: java后端 专题: 源码阅读 标签: spring,事务,orm

异步任务中 Hibernate 获取当前 Session 失败:Could not obtain transaction-synchronized Session for current thread

环境:Spring 4.3.9、Hibernate 4.3.10、MyBatis-Spring 1.3.3。
Hibernate 使用 Spring 的 SpringSessionContext,异步任务通过 ThreadExecutorUtilWithShiro.execute(...) 提交。
本文只讨论这一组合下使用 SessionFactory.getCurrentSession() 的场景。

先说结论

核心原因:异步线程调用 getCurrentSession() 时,没有已绑定的 Session,事务同步也未激活,SpringSessionContext 无法提供当前 Session。

原线程的事务不会传给异步线程。让异步线程中的数据库操作进入有效的 Spring 事务,即可解决本例问题。

从源码分支看原因

SpringSessionContext.currentSession() 的关键分支可概括为以下伪代码(省略已绑定 Session 的具体类型处理及其他上下文分支):

java 复制代码
从 TransactionSynchronizationManager 获取 SessionFactory 对应的线程资源
已有绑定的 Session -> 返回 Session
否则,isSynchronizationActive() == true
  -> openSession() -> 绑定到线程 -> 注册清理回调 -> 返回 Session
否则 -> 抛 HibernateException: Could not obtain transaction-synchronized Session for current thread

这里的 isSynchronizationActive() 表示当前线程启用了 Spring 事务同步,使新 Session 可以绑定到线程,并在同步结束时清理;它不等同于 isActualTransactionActive()(确实存在数据库事务)。
例如某些 SUPPORTS 配置也可能激活事务同步,却没有实际数据库事务。

本文的写入场景应建立真正的数据库事务,而不是只求这个判断返回 true

为什么不能在 false 时自动打开?因为 getCurrentSession() 的语义是取得由框架管理生命周期的 Session。
如果没有绑定资源,也没有事务同步,框架无法保证谁来负责 flush、提交/回滚、关闭及归还连接。
需要自行管理生命周期时可以显式使用 openSession(),但那是另一套用法,不是此处的替代修复。

异步调用把这个判断变成了 false

text 复制代码
调用线程:经过事务代理 -> Spring 在该线程建立事务和 Session
                      -> ThreadExecutorUtilWithShiro.execute(task)
线程池线程:执行 task -> 直接调用同类方法(绕过代理)
                     -> Hibernate getCurrentSession() -> 无绑定 Session、无事务同步 -> 异常

即使项目配置了 XML txAdvice 或方法上标了 @Transactional,它们也只在调用经过 Spring 代理时生效;private 方法或 this.method() 形式的同类调用不会因事务配置自动开启事务。
ThreadExecutorUtilWithShiro 传递的是 Shiro 上下文,不传递原线程的事务或 Session。

这也解释了为何其他同样使用 execute 的任务可能没事:它们在异步线程中又进入了被代理的事务方法,或者根本没调用 Hibernate 的 getCurrentSession()

解决方案:在异步线程内建立事务

1 、让 Spring 事务配置或注解真正生效。

在异步任务中通过 Spring 代理调用事务方法,确保 XML txAdvice 的切点/方法名规则命中,或 @Transactional 生效。
常见失效情况有 private 方法、this.method() 同类自调用,以及直接调用未经过 Spring 代理的对象。
适合已有声明式事务配置的项目。

2 、手动开启 Spring 事务。

在异步任务中用 TransactionTemplate.execute(...) 包住数据库操作,模板使用管理对应 Hibernate SessionFactory 的事务管理器。
适合需要在当前任务中明确控制事务范围、而不方便拆出代理方法的情况。

无论哪种方案,事务都应从异步线程中首次依赖 getCurrentSession() 的操作之前开始,并覆盖需要同一事务的数据库操作。
耗时的远程调用尽量放在事务外。

为什么 MyBatis 查询往往没事?

​ 常见的 Spring + MyBatis 调用走 SqlSessionTemplate:通过 SqlSessionUtils 获取 SqlSession
已有事务同步且满足条件时,它会复用并注册事务关联的 Session;没有事务同步时,也可以为这一次 Mapper 调用创建 Session,调用完提交(如有必要)并关闭
所以普通无事务查询往往能成功,不会走 Hibernate SpringSessionContext 的检查。

但“能查询”不等于“有事务”。无事务的多次 Mapper 写入可能分别提交,后一步失败不能回滚前一步;事务里混用不兼容的执行器配置还可能直接报错。
连接故障、SQL/映射错误、手工管理的 Session 已关闭等也会导致 MyBatis 操作失败。

需要多步原子性时,仍须在异步线程里建立事务;混用 MyBatis 和 Hibernate 时还要核对数据源及事务管理器是否让两者真正参与同一事务。

放到现在的 Spring Boot 项目里

线程切换不传递事务、同类自调用绕过代理,这两个注意点仍适用。

但不能推断所有新项目都报相同异常:Spring Boot 常见的 JPA EntityManager 与 Hibernate 原生 getCurrentSession() 不是同一入口;JPA 的非事务查询可能执行,而写入/flush 等可能因缺少事务报错。
使用 SqlSessionTemplate 的 MyBatis 单次查询通常仍可无事务执行。
具体错误取决于 ORM 接口和事务配置;跨异步线程的多步数据库写入,仍应明确新线程内的事务边界。

评论区

avatar
未登录

暂无评论,来发表第一条评论吧~