第一次看到这句 SqlSession was not registered for synchronization because synchronization is not active,我正在帮一个老项目排查连接数偏高的问题。日志平台上一搜,这个关键词刷了满屏,业务日志里本该出现的 SQL 反而被顶得看不见了。请求能正常返回,功能看起来没坏,但谁也不敢拍胸脯说没问题。
这句话虽然长得像报错,实际上在多数场景只是 SqlSession 没有进入 Spring 事务同步机制的提示。真正要命的不是这句日志本身,而是它背后可能藏着的“事务根本没生效”这类隐蔽问题。这篇内容我不打算只解释英文意思,我会按一次真实排查的思路,把这个日志涉及的 Spring 事务边界、MyBatis 会话机制、以及几个最容易踩的坑一起拆开讲清楚,末尾附上可直接照做的问题速查表。
1. 先别慌:这段日志到底在说什么
1.1 按字面理解和按代码理解是两回事
英文字面意思很好翻译:SqlSession 没有注册到同步机制里,因为同步机制当前没有激活。也就是说 MyBatis 在执行数据库操作时,想把自己创建的 SqlSession 挂到当前线程的事务资源上,但发现当前并没有一个可以挂靠的事务同步环境,于是打了一条说明日志。
这里的“同步”不是多线程同步,也不是主从数据库复制同步,而是 Spring 的事务同步机制。Spring 在开启一个事务时,会在当前线程保存一组状态,并在事务提交或回滚前后触发一系列回调。MyBatis 与 Spring 整合后,会把自己的 SqlSession 注册到这个机制里,目的就是让 SqlSession 的生命周期和事务完全绑定:事务提交它就跟着提交,事务回滚它就跟着回滚,而不是每次 Mapper 调用都自行开关连接。
如果同步机制没有激活,MyBatis 拿不到这个可以挂靠的事务上下文,就会打印上面这句话。简单讲,日志在告诉你:这次数据库调用没有纳入一个处于活跃状态的 Spring 事务同步范围。
1.2 出现频繁的几类现场
结合我接触过的项目,这句日志高频出现的场景基本能归成几类:
- 某个 Service 方法没有加
@Transactional,方法内部直接调 Mapper。 - 加了
@Transactional,但由于同类自调用、非 public 方法等原因,注解没有真正被 Spring 事务切面拦到。 - 定时任务、MQ 消费线程里直接调 Mapper,又没有手动开启事务。
- 开发环境与生产环境的日志级别不同,导致你只在某个环境看到它。
- 项目依赖里没有正确配置事务管理器,所有标注了
@Transactional的方法实际都没有事务。
换句话说,这条日志的“浓度”,基本能反映代码里真正被 Spring 事务管理的比例。
1.3 这句话是错误吗?要不要马上处理
先说结论:单独一行这个日志,通常不是致命错误。Spring Boot 默认日志门面下,这句话在被打印出来的版本里一般只是普通 INFO 级别,有些新版本甚至降到了 DEBUG。它出现时 Mapper 的 SQL 依然会执行,本次调用大概率也能正常返回结果。
但它是一个值得重视的信号。如果你系统里频繁出现这行日志,且你心里预期很多方法都应该有事务保护,那就要检查事务链路了。反之,如果某些数据查询本来就只是只读操作,你也刻意不想让它开事务,那么这句话在这里出现就是正常的。
为了不绕弯子,我先把一个核心结论放在前面:看到这句话,第一反应不是去调整日志级别把它藏起来,而是先确认当前这条调用路径上到底有没有事务、事务为什么没生效。
2. 从一次 Mapper 调用说起:MyBatis 会话是怎么被 Spring 管的
2.1 Mapper 接口背后不是一路直接执行 SQL
很多人刚接触 MyBatis 时会以为 Mapper 接口就是 JDBC 的简单包装,调用一个方法就执行一条 SQL。实际上如果你用的是 mybatis-spring,情况要绕一层:
Mapper 接口的实例是 Spring 通过动态代理生成的。你在 Service 里注入的 UserMapper,真正执行时的调用链大致是:
Service -> MapperProxy -> SqlSessionTemplate -> SqlSessionUtils -> SqlSessionFactory -> Executor -> JDBC
SqlSessionTemplate 是个复合角色:一方面它实现了 SqlSession 接口,另一方面它内部是线程安全、可以在多线程环境共享的。它不能像原生 MyBatis 那样让某个 DefaultSqlSession 跨方法持有太久,因为 SqlSession 本身不是线程安全的。所以 SqlSessionTemplate 采取的策略是:每次进入 Mapper 方法时,先看看当前线程有没有一个已经开始执行且可以复用的 SqlSession;有就共享,没有就临时创建一个,用完再关。
这一步在源码里对应 SqlSessionTemplate 内部对 SqlSessionUtils.getSqlSession() 的调用。正是这个 getSqlSession 方法,负责判断当前能否复用会话。
2.2 没有事务时到底发生了什么
在没有 Spring 事务保护的情况下,比如普通方法直接调 userMapper.selectById(id),执行过程大致是:
SqlSessionTemplate检查当前线程有没有已经注册的 SqlSession。- 检查后发现没有,于是利用
SqlSessionFactory打开了一个新的DefaultSqlSession。 - 由于当前线程没有处于活跃的事务同步状态,SqlSession 不会注册到 Spring 的同步机制,日志自然就打印出来了。
- Mapper 方法执行完成,SqlSession 被
closeSqlSession关闭,底层数据库连接归还连接池。
如果把日志级别调高一点,你会看到类似这样的输出:
text复制Creating a new SqlSession
SqlSession was not registered for synchronization because synchronization is not active
Closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@36a2f7a1]
这里 Creating a new SqlSession 和 Closing SqlSession 对应当前调用真的是“开一个、用一个、关一个”。一旦代码里有很多这样的调用,连接池的获取和释放就会非常频繁,系统压力一大,连接等待和性能问题就会跟着冒头。
2.3 有事务时会怎么样
反过来,如果当前方法确实被 Spring 事务管理,比如调用链进入了某个标注了 @Transactional 的 Service 方法,并且事务切面正常生效,那么在事务刚开始时 Spring 会调用 TransactionSynchronizationManager 的初始化逻辑,在当前线程激活同步机制,并绑定事务资源。
此时再执行 Mapper 方法,SqlSessionUtils 检查到同步处于活跃状态,就会执行注册动作:把这个 SqlSession 挂到当前事务上,并注册事务同步回调。对应的日志通常就不再有“not registered”这句,取而代之的是:
text复制Creating new transaction with name [com.example.demo.DemoServiceImpl.innerMethod]
Registering transaction synchronization for SqlSession
... SQL 执行 ...
Releasing transactional SqlSession
Transaction synchronization deregistering SqlSession
Transaction synchronization closing SqlSession
注意这里 Session 的生命周期变长了:它在事务开始时出现,在整个事务执行过程中一直存活,直到事务提交或回滚结束后才被释放。这样的事务模式下,多个 Mapper 方法共享同一个 SqlSession 和同一个底层连接,提交和回滚才会真正服从同一个事务边界。
有人形容这两类模式的差别,就好像是同样的几个操作,一个是你每次做一步就完全重新开张一次店铺,另一个是提前约定好“今天这单整体算账”。这个类比虽然粗糙,但很能说明问题。
2.4 关键是 TransactionSynchronizationManager 的活跃状态
不管是日志出现还是没有出现,背后的判断核心都围绕 TransactionSynchronizationManager.isSynchronizationActive() 这个布尔值。
TransactionSynchronizationManager 是 Spring 事务框架中一个相当底层的组件,它负责管理每个线程的事务资源和同步回调。每个线程有自己独立的一份 ThreadLocal 状态。也就是说,同步是否激活,并不是一个全局开关,而是和当前执行线程强相关的。
如果一个请求是从 Spring 事务代理方法进来的,那么事务同步状态就已经在这个线程里初始化好了;如果进入 Mapper 之前没有任何事务逻辑,这个状态就是 false。这也是为什么同样的代码,在同一个事务里调用 Mapper 时不打日志,换到另一个线程池任务里执行就直接打了出来,因为那个线程根本没有被任何事务初始化过。
3. @Transactional 看起来生效,同步却不激活?重点查这几个点
3.1 同类自调用是隐藏最深的坑
我排查过很多次代码后有一种感觉:多数人看到这句日志,第一反应是去查自己的方法有没有加注解。结果一看,方法上明明写着 @Transactional,于是就开始怀疑是不是 MyBatis 配置有问题。但真实原因往往更尴尬:注解加了,但这个方法不是通过 Spring 代理被调用的。
最典型的情况是同类里的自调用:
java复制@Service
public class OrderService {
@Transactional
public void createOrder(Order order) {
// 一系列数据库操作
}
public void handleOrder(Order order) {
order.setStatus(1);
this.createOrder(order); // 看起来调用了事务方法,其实绕过代理
}
}
当外部调用 handleOrder 时,Spring 会把请求交给 OrderService 的代理对象。但 handleOrder 方法执行到 this.createOrder(order) 时,this 指向的是当前业务对象本身,不是 Spring 帮我们生成的代理对象。createOrder 注解上的 @Transactional 需要经过事务切面才会生效,而这一步在自调用场景里被直接跳过了。
解决办法通常是注入自身代理:
java复制@Service
public class OrderService {
@Autowired
@Lazy
private OrderService self;
@Transactional
public void createOrder(Order order) {
// 一系列数据库操作
}
public void handleOrder(Order order) {
order.setStatus(1);
self.createOrder(order);
}
}
或者把事务方法独立到另一个 Bean 里去,让 AOP 能被正常触发。
3.2 方法不是 public、类没被 Spring 管理
Spring 声明式事务默认基于动态代理实现,无论是 JDK 动态代理还是 CGLIB,都要求被拦截的方法具备可代理的条件。如果一个标注了 @Transactional 的方法不是 public 的,代理机制很多时候根本不会把事务切面织入进去。
还有更基础的问题:这个 Service 类本身到底是不是被 Spring 容器管理的 Bean?如果这个对象是你自己 new 出来的,或者在一个不是 Spring 管理的模块里被直接创建,那无论方法上写了多少注解,在它身上都完全没有意义。
我见过有的遗留项目在配置类里手动创建 Service 对象,还通过静态方法到处共享。这样的设计下,即使代码里全是 @Transactional,事务也可能一个都不生效。碰到这类情况,第一步不是看 @Transactional 写没写,而是顺着调用链确认当前对象是不是从 Spring 容器里取出来的代理对象。
3.3 事务管理器是否真的存在
Spring Boot 项目一般会自动配置 DataSourceTransactionManager,前提是 classpath 中存在对应的 JDBC 事务依赖。如果工程项目比较个性化,比如你手工整合 Spring 和 MyBatis,但又没有配置事务管理器,那么所有 @Transactional 都会像没写一样;甚至连启动时都可能遇到针对事务管理器的报错。
确认方式比较简单。一是看能否从容器里拿到 PlatformTransactionManager,二是直接把日志级别开大:
yaml复制logging:
level:
org.springframework.jdbc.datasource.DataSourceTransactionManager: DEBUG
正常开启事务时,控制台会打印类似 Creating new transaction with name ... 的日志。如果整个请求过程中连这种日志都没有,那问题基本可以确定在事务链路本身,而不是 MyBatis。
3.4 多线程和异步调用会丢失事务上下文
还有一个非常容易忽略的情况:事务同步状态和线程绑定。只要一跨线程,事务上下文基本就丢了。
比如外面有一个 @Transactional 方法,里面用 CompletableFuture.runAsync(...) 或者自己 new 了一个线程去执行数据库操作。子线程执行 Mapper 时,TransactionSynchronizationManager 里不会自动继承父线程的事务状态。如果子线程里没有自己开启新事务,那么它就必然处于“同步不激活”的状态,这句日志照打不误。
不要指望父线程开事务、子线程的 SQL 也能跟着一起提交回滚,设计上就不是这么工作的。如果真的需要子线程参与同一个事务,要么把业务拆成一个大事务串行执行,要么使用专门设计的编程式事务方案,并在子线程里手动管理事务边界。
3.5 事务传播级别带来的“假象”
@Transactional 默认传播级别是 REQUIRED,也就是有事务就加入,没有就新建。但如果在调用链中某处使用了 REQUIRES_NEW、NOT_SUPPORTED 或 NEVER 等传播属性,情况会完全不同。
拿 NOT_SUPPORTED 举例:它会让当前事务先挂起,然后以无事务方式执行方法。此时 MyBatis 执行到 SqlSessionUtils 时同样会发现同步状态不是正常情况下可注册的状态,这句日志同样可能出现。这类写法通常用于某些不希望被长事务拖住的查询场景,算是有意为之,日志出现并不意外。
判断是否属于这类情况,要结合调用链上的事务传播配置一起看,不能只盯着一行日志。
4. 实操:复现一次“根本没有事务”的调用,然后把链路看明白
4.1 搭一个最小验证工程
比起反复翻源码,我更推荐直接搭一个最小工程来观察。依赖就用 spring-boot-starter-web、mybatis-plus-boot-starter 或者 mybatis-spring-boot-starter 均可,再配上 h2 数据库,一会儿就能跑起来。
建一张简单用户表,写一个 Mapper:
java复制public interface UserMapper {
int insert(User user);
}
对应一个 Service:
java复制@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public void insertWithNoTransaction() {
userMapper.insert(new User("no-tx"));
}
@Transactional
public void insertWithTransaction(String name) {
userMapper.insert(new User(name));
}
}
然后用一个 CommandLineRunner 分别调用这两个方法。注意项目里要开启事务管理,Spring Boot 项目中通常无需额外配 @EnableTransactionManagement,它会在检测到事务管理器和相关切面后自动开启。
4.2 调整日志级别让关键节点现身
日志级别配置很关键。我建议把下面几个命名空间的日志临时调到 DEBUG:
yaml复制logging:
level:
org.springframework.jdbc.datasource.DataSourceTransactionManager: DEBUG
org.mybatis.spring.SqlSessionUtils: DEBUG
org.mybatis.spring.SqlSessionTemplate: DEBUG
这里能看到的东西非常有价值:DataSourceTransactionManager 负责展示事务开启和提交,SqlSessionUtils 负责展示会话创建、注册、关闭。两条链路合在一起看,事务边界与 SqlSession 生命周期之间的关系就非常清楚了。
如果你用的 mybatis-spring 版本不同,日志 logger 名字略有差异,但大体都在 org.mybatis.spring 包下。把包级别调成 DEBUG 可能会有大量输出,不过风险也就是日志多,随便看看不成问题。
4.3 观察结果:两种日志行程线
先调用 insertWithNoTransaction()。控制台会依次出现:
text复制Creating a new SqlSession
SqlSession was not registered for synchronization because synchronization is not active
Preparing: insert into user(name) values (?)
...
Closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@36a2f7a1]
再调用 insertWithTransaction()。如果 @Transactional 生效,日志会多出事务开启的部分,而且几乎不会出现“not registered”:
text复制Creating new transaction with name [com.example.demo.UserService.insertWithTransaction]
Creating a new SqlSession
Registering transaction synchronization for SqlSession
Preparing: insert into user(name) values (?)
...
Releasing transactional SqlSession
Transaction synchronization deregistering SqlSession
Transaction synchronization closing SqlSession
Committing JDBC transaction ...
两组日志一对比,应该能很直观地感觉到“同步注册”和“未注册”在行为上的差异。
4.4 再验证一次自调用导致的事务失效
为了让自己对事务失效有身体记忆,可以在同一个 Service 里加上自调用场景:
java复制@Transactional
public void innerCreate(String name) {
userMapper.insert(new User(name));
throw new RuntimeException("rollback test");
}
public void outerCallSelf() {
this.innerCreate("self");
}
调用 outerCallSelf() 后再去查数据库,你会发现名字为 self 的数据已经插进去了。因为 innerCreate 上的事务没有生效,异常发生时没有任何事务帮你回滚。此时日志里也会出现那句经典的 not registered。
这个操作我建议所有维护 Spring + MyBatis 项目的人都实际跑一遍。只要亲眼看一次数据没有被回滚成功,对这个日志的警惕心就会长期保持。
5. 排查速查与几个容易走偏的小提醒
5.1 一句话速查表
这里整理成了一份表格,方便后面遇到类似日志时对号入座。
| 现象 | 日志特征 | 大概率原因 | 处理方向 |
|---|---|---|---|
| 普通方法直接调 Mapper | Creating a new SqlSession 后紧跟 not registered | 该路径无事务 | 根据业务决定是否需要加 @Transactional |
| Service 上有注解但日志依旧 | 没有 Creating new transaction 日志 | 事务代理未生效 | 查自调用、非 public、类是否被 Spring 管理 |
| 启动后控制台大量刷这句话 | 基本每个 Mapper 调用都出现 | 大批业务代码缺乏统一事务 | 梳理核心写接口,补充事务边界 |
| 使用异步线程执行 Mapper | 线程内出现这句话 | 跨线程丢失事务上下文 | 在子线程内独立开启事务或重试设计 |
| 方法有意使用 NOT_SUPPORTED | 只在该场景出现 | 传播级别设置导致 | 正常,不用处理 |
| 大数据量分页查询 | 偶发出现 | 非事务查询 | 如果不要求强一致可忽略 |
5.2 哪些情况不用改,哪些情况必须改
有一些只读查询,本身就没有修改多条数据的需求,加事务反而会因为长时间持连接而影响并发,这种情况下看到这句日志可以不用太紧张。但要注意,即使不加事务,SqlSessionTemplate 仍然会保证 Mapper 方法内 SQL 的正常执行,SQL 执行完成后连接会回到连接池,不会出现连接泄漏。
必须认真处理的是以下几种情况:
- 事务方法里包含了多次写操作,但你发现其中一段根本没有被事务保护。
- 一次请求里包含多个 Mapper 写操作,但每个写操作都各自开关连接,出现明显的连接获取开销。
- 明明在核心业务方法上加了
@Transactional,但异常发生后数据照常入库,说明事务代理链路出了问题。 - 想通过某个事务统一管理多条 SQL 的一致性,却发现其中一个查询在事务之外执行。
如果不确定该不该加事务,可以从业务约束反推:如果第二个 SQL 执行失败,前面第一个 SQL 是否需要被撤销?需要,就必须把这两个操作放进同一个事务边界。不需要,那它们各自独立提交也说得过去。
5.3 别被搜索时搜出的其他技术名词带偏
最近我在整理这个问题的资料时,看到不少搜索记录里把 SqlSession was not registered for synchronization because synchronization is not active 和类似 DefaultSqlSession@36a2f7a1 的对象引用、甚至 no server suitable for synchronization found 这样的句子混在一起。
这里要稍微提醒一下:带 DefaultSqlSession@xxx 的输出通常只是调试日志里打印了 SqlSession 实例对象,如果没有完整的异常栈,它不能说明会话被错误复用或泄漏。而 no server suitable for synchronization found 这类“找不到可同步服务器”的信息,上下文和 MyBatis 这条日志差别很大,更像集群复制、客户端同步或网络搜索类的问题描述。排查时必须把完整堆栈拿出来看,别只盯着几个英文关键词就急着改代码,那样很容易把不相干的两个问题搅到一起。
5.4 把这句话当成一个“哨兵”而不是一个故障
我个人的经验是:这句日志最值得利用的地方,是它可以作为项目事务覆盖率的粗糙指标。
如果一个服务以前很少出现这句话,某次发布后突然大幅增加,那几乎可以断定新代码里有一批方法没有走预期事务。如果这句话集中在某些只在异步线程池里运行的任务路径上,也可以提醒你去看看这些任务的写入场景是否允许丢失事务保护。把它当成故障去消灭,问题不大;真正把它当观察信号用起来,才对生产系统有价值。
实际改代码时我也会刻意留一条习惯:凡是写了 @Transactional 的方法,顺手再确认一下它是不是只能通过 Spring 代理从类外部进入。如果代码里有自调用嫌疑,哪怕现在没出问题,我也会改成注入代理或者抽 Service 的方式。毕竟多数跳进这个日志坑的项目,最后都不是栽在 MyBatis 配置上,而是栽在“以为有事务、实际没有”的错觉上。
