第一次看到这句 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 方法,负责判断当前能否复用会话。
