第九篇:SqlSession 为什么不是线程安全的?Spring 为什么还能放心共享 Mapper?
2026/6/27 2:04:36 网站建设 项目流程

SqlSession 为什么不是线程安全的?Spring 为什么还能放心共享 Mapper?

MyBatis 系列写到这里,我们已经知道:

  • Mapper 为什么没有实现类,却能执行 SQL
  • MyBatis 为什么知道执行哪条 SQL
  • 一条 SQL 在 MyBatis 里的完整执行流程
  • 查询结果如何映射成对象
  • 插件、缓存又是如何工作的

很多人这时候都会产生一个疑问。

项目里,我们每天都是这样写的:

@AutowiredprivateUserMapperuserMapper;

整个项目只有一个UserMapperBean。

无数个请求同时调用:

userMapper.selectById(1L);

却几乎从来不会出现线程安全问题。

但是,MyBatis 官方文档却明确说明:

SqlSession 不是线程安全的。

既然如此,为什么 Spring 还能把 Mapper 当成单例 Bean 来使用?

今天,我们就把这个问题彻底讲明白。


先说结论

很多人一直有一个误区:

Mapper 是线程安全的。

其实并不是。

真正线程安全的是:

Mapper(代理对象) ↓ SqlSessionTemplate ↓ 当前线程自己的 SqlSession

也就是说:

共享的是 Mapper,不是 SqlSession。


什么是 SqlSession?

很多人把 SqlSession 理解成数据库连接。

其实并不准确。

SqlSession 更像是:

一次数据库会话。

我们平时调用:

Useruser=userMapper.selectById(1L);

最终都会进入:

SqlSession

例如:

sqlSession.selectOne(...)

再继续往下:

SqlSession ↓ Executor ↓ StatementHandler ↓ JDBC

前面《一条 SQL 在 MyBatis 里到底经历了什么?》已经分析过这条调用链。

所以:

SqlSession 是整个 MyBatis 执行流程的入口。


为什么它不能共享?

看看 DefaultSqlSession。

源码里面保存了很多运行状态。

例如:

privatefinalConfigurationconfiguration;privatefinalExecutorexecutor;privatebooleandirty;

尤其是:

Executor

Executor 里面又维护着:

一级缓存 事务状态 数据库连接 执行上下文

这些都是一次数据库会话的数据。

如果两个线程同时共享一个 SqlSession。

例如:

线程 A:

selectById()

线程 B:

updateUser()

它们可能:

  • 共用一级缓存
  • 共用事务
  • 共用 Connection
  • 同时修改 Executor 状态

整个会话状态都会混乱。

所以:

SqlSession 天生就不能设计成线程安全。


如果共享,会发生什么?

假设:

SqlSessionsqlSession=sqlSessionFactory.openSession();

然后:

ThreadA

调用:

sqlSession.selectList(...)

与此同时:

ThreadB

调用:

sqlSession.commit();

这时候:

线程 A 查询还没结束。

线程 B 已经提交事务。

甚至关闭了连接。

结果可想而知。

所以:

一个 SqlSession,只能属于一个线程。


那为什么 Mapper 可以共享?

真正神奇的地方就在这里。

Spring 注入的其实不是:

DefaultSqlSession

而是:

SqlSessionTemplate

很多人从来没见过这个类。

但它才是 MyBatis 和 Spring 整合最关键的一层。

整个调用关系大概是这样:

Controller │ ▼ Mapper(JDK 动态代理) │ ▼ SqlSessionTemplate │ ▼ 当前线程 SqlSession │ ▼ Executor │ ▼ JDBC

真正共享的是:

SqlSessionTemplate

而不是:

DefaultSqlSession

SqlSessionTemplate 做了什么?

每次执行 Mapper 方法。

都会进入:

SqlSessionTemplate

然后根据当前线程,获取属于自己的 SqlSession。

核心逻辑可以理解成:

SqlSessionsqlSession=SqlSessionUtils.getSqlSession(...);

如果当前线程:

已经存在 SqlSession直接复用。如果没有创建新的 SqlSession。

整个过程和线程绑定:

线程 A:

SqlSession A

线程 B:

SqlSession B

线程 C:

SqlSession C

每个线程都有自己的 SqlSession。

互不影响。


ThreadLocal 才是真正的关键

很多人以为:

Spring 给 Mapper 加锁了。

其实根本没有,真正做到线程隔离的是:

ThreadLocal

Spring 会把当前线程对应的 SqlSession 保存起来。

可以理解成:

Thread A │ ▼ SqlSession A Thread B │ ▼ SqlSession B Thread C │ ▼ SqlSession C

所以虽然 Mapper 是单例。

但是每个线程拿到的 SqlSession 都不一样。

自然也就不会发生线程安全问题。


为什么事务还能共用一个 SqlSession?

很多人又会继续问。

一个事务里面连续执行多个 Mapper。

为什么还是同一个 SqlSession?

例如:

@Transactionalpublicvoidsave(){userMapper.insert(...);orderMapper.insert(...);}

答案还是:

ThreadLocal。

事务开启以后Spring 会把 SqlSession 绑定到当前线程。

整个事务期间所有 Mapper,都会拿到同一个 SqlSession。

于是:

  • 一级缓存可以共享
  • Connection 可以共享
  • 事务也保持一致

直到事务结束。

SqlSession 才会释放。


为什么官方一直强调不要自己保存 SqlSession?

有些人喜欢这样写:

publicclassUserDao{privateSqlSessionsqlSession;}

这样做非常危险。

因为:

这个 SqlSession 很可能会被多个线程同时使用。

正确方式永远都是:

SqlSessionFactoryopenSession()

或者:

直接交给 Spring。

不要自己缓存 SqlSession。


总结

SqlSession 不是线程安全。

不是因为代码写得不好。

而是因为:

它本来就代表一次数据库会话。

会话里面保存了:

  • Executor
  • 一级缓存
  • Transaction
  • Connection

这些状态天然不能共享。

真正做到线程安全的。

不是 SqlSession。

而是:

SqlSessionTemplate + ThreadLocal。

Spring 共享的是 Mapper。

而每个线程真正使用的,却始终是属于自己的 SqlSession。

这也是为什么:

我们每天放心注入一个 Mapper。

却从来不用担心并发问题。


上一篇:《为什么很多公司禁用 MyBatis 二级缓存?》

下一站:《Redis 为什么这么快?它真的只是因为内存吗?》


如果这篇文章让你真正理解了Mapper 为什么能单例,而 SqlSession 却不能共享,欢迎点个赞👍。

你也可以在评论区聊聊:你以前是不是一直以为 Mapper 和 SqlSession 是同一个东西?

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询