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;尤其是:
ExecutorExecutor 里面又维护着:
一级缓存 事务状态 数据库连接 执行上下文这些都是一次数据库会话的数据。
如果两个线程同时共享一个 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而不是:
DefaultSqlSessionSqlSessionTemplate 做了什么?
每次执行 Mapper 方法。
都会进入:
SqlSessionTemplate然后根据当前线程,获取属于自己的 SqlSession。
核心逻辑可以理解成:
SqlSessionsqlSession=SqlSessionUtils.getSqlSession(...);如果当前线程:
已经存在 SqlSession直接复用。如果没有创建新的 SqlSession。
整个过程和线程绑定:
线程 A:
SqlSession A线程 B:
SqlSession B线程 C:
SqlSession C每个线程都有自己的 SqlSession。
互不影响。
ThreadLocal 才是真正的关键
很多人以为:
Spring 给 Mapper 加锁了。
其实根本没有,真正做到线程隔离的是:
ThreadLocalSpring 会把当前线程对应的 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 很可能会被多个线程同时使用。
正确方式永远都是:
SqlSessionFactory↓openSession()或者:
直接交给 Spring。
不要自己缓存 SqlSession。
总结
SqlSession 不是线程安全。
不是因为代码写得不好。
而是因为:
它本来就代表一次数据库会话。
会话里面保存了:
- Executor
- 一级缓存
- Transaction
- Connection
这些状态天然不能共享。
真正做到线程安全的。
不是 SqlSession。
而是:
SqlSessionTemplate + ThreadLocal。
Spring 共享的是 Mapper。
而每个线程真正使用的,却始终是属于自己的 SqlSession。
这也是为什么:
我们每天放心注入一个 Mapper。
却从来不用担心并发问题。
上一篇:《为什么很多公司禁用 MyBatis 二级缓存?》
下一站:《Redis 为什么这么快?它真的只是因为内存吗?》
如果这篇文章让你真正理解了Mapper 为什么能单例,而 SqlSession 却不能共享,欢迎点个赞👍。
你也可以在评论区聊聊:你以前是不是一直以为 Mapper 和 SqlSession 是同一个东西?