先给你吃颗定心丸:这句 SqlSession was not registered for synchronization because synchronization is not active 不是所有环境里的致命错误。很多项目里它只是日志被调到 DEBUG 后出现的一行提示,代码照样跑得飞快。但是,如果你是在做批量更新、读写分离、或者多个 Mapper 操作需要“要么全成功、要么全回滚”的业务里看到它,那就不能只是“哦,有意思”就划过去,因为它背后暴露的,往往是 Spring 事务没有生效,MyBatis 的 SqlSession 正在脱离事务管理,每一次 Mapper 调用都可能单独提交,一致性风险已经埋下了。
我遇到这行日志的次数,不比 NullPointerException 少。这篇文章会老老实实拆清楚:这是哪段代码打出来的、它为什么出现、什么时候可以无视、什么时候必须修,以及几种最实际的修复方式。你可以直接对照自己的日志上下文来判断,不用再去搜一堆模棱两可的回答。连我在排查初期都因为搜到 no server suitable for synchronization found 这种完全不相干的 NTP 时间同步报错而分心过,所以这次一并帮你把弯路标出来。
1. 先定位:这行日志到底是谁打印的
1.1 它来自 MyBatis-Spring,不是 MyBatis 核心
很多同事第一反应是去 Google 搜“MyBatis synchronization”,结果先看到 no server suitable for synchronization found,以为系统时间同步出问题,或者数据库主从同步出问题。我第一次排查时也被误导过,折腾了十分钟才发现这行日志和 NTP、数据库复制都没关系。
它真正的来源是 org.mybatis.spring.SqlSessionUtils 这个类。只要你在 Spring Boot 项目里使用了 mybatis-spring-boot-starter,或者手动引入了 mybatis-spring,并且通过 SqlSessionTemplate 执行 Mapper 方法,就可能走进这段逻辑。具体打日志的方法在我的环境里是 SqlSessionUtils.registerSessionHolder(),在 MyBatis-Spring 2.x 里它通常是 DEBUG 级别,但正是因为 Spring Boot 全家桶常常把 org.mybatis.spring 调低日志级别,这行提示才会出现在控制台。
1.2 直接堆栈 vs 日志上下文,先分清谁在报警
不要只看一行日志就下结论,先看它前后有没有异常堆栈。
如果你看到的完整日志大致是这样:
text复制Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@36a2f7a1] was not registered for synchronization because synchronization is not active
Closing SqlSession
这是无事务环境下的正常活动日志。DefaultSqlSession@36a2f7a1 就是默认的 SqlSession 实现,后面的 @ 加对象哈希是一串自动生成的编号,没有业务含义。它创建了、用了、关闭了,全程没有进入 Spring 的事务同步注册流程,所以打印这句话。
如果你看到的是:
text复制org.springframework.transaction.TransactionSystemException: Could not commit JDBC transaction
Caused by: java.sql.SQLException: ...
那才是需要处理的错误。前一句只是提示,后面的异常才是事故。你真正要分析的是异常栈,而不是把前面那句“同步未激活”当成根本原因。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Spring 事务和 SqlSession 的绑定机制,为什么需要 synchronization
2.1 原生的 MyBatis 使用方式,本来是一手交钱一手交货
为了讲清楚 synchronization,我先说下没有 Spring 时 MyBatis 是怎么工作的。传统用法是你手动拿到一个 SqlSession:
java复制try (SqlSession session = sqlSessionFactory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
mapper.updateName(1, "张三");
session.commit();
}
在这段代码里,session 你自己开,commit() 你自己调,close 你自己管。如果中间调了两个 Mapper 方法,它们在同一个 SqlSession 里,也用同一个数据库连接。连接打开期间有事务边界,要么一起提交,要么一起回滚,这个模型很容易理解。
但把 MyBatis 塞进 Spring 后,我们不能让每个业务方法都自己管理 SqlSession。那样的话,每个 Service 方法都得写一堆 try-catch-commit-rollback,和 Spring 声明式事务完全脱节。而且 Spring 的 @Transactional 希望把一个事务边界内的多个 DAO 调用放进同一个数据库连接,你手动开 session 根本没法参与。
2.2 SqlSessionTemplate 和 Mapper 的动态代理,才是幕后主角
mybatis-spring 给出的方案是 SqlSessionTemplate。它不是你自己 new 出来的 DefaultSqlSession,而是一个线程安全的代理对象,内部通过 JDK 动态代理拦截所有 Mapper 方法。业务代码里注入 Mapper 接口,而 Mapper 接口在 Spring 容器中的实际实现,最终都会调用 SqlSessionTemplate。
当 Mapper 方法被执行时,真正干活的是 org.mybatis.spring.SqlSessionTemplate.SqlSessionInterceptor。它会做几件事:
- 调用
SqlSessionUtils.getSqlSession()获取一个 SqlSession。 - 执行真正的
