
SqlSession 为什么不是线程安全的Spring 为什么还能放心共享 MapperMyBatis 系列写到这里我们已经知道Mapper 为什么没有实现类却能执行 SQLMyBatis 为什么知道执行哪条 SQL一条 SQL 在 MyBatis 里的完整执行流程查询结果如何映射成对象插件、缓存又是如何工作的很多人这时候都会产生一个疑问。项目里我们每天都是这样写的AutowiredprivateUserMapperuserMapper;整个项目只有一个UserMapperBean。无数个请求同时调用userMapper.selectById(1L);却几乎从来不会出现线程安全问题。但是MyBatis 官方文档却明确说明SqlSession 不是线程安全的。既然如此为什么 Spring 还能把 Mapper 当成单例 Bean 来使用今天我们就把这个问题彻底讲明白。先说结论很多人一直有一个误区Mapper 是线程安全的。其实并不是。真正线程安全的是Mapper代理对象 ↓ SqlSessionTemplate ↓ 当前线程自己的 SqlSession也就是说共享的是 Mapper不是 SqlSession。什么是 SqlSession很多人把 SqlSession 理解成数据库连接。其实并不准确。SqlSession 更像是一次数据库会话。我们平时调用UseruseruserMapper.selectById(1L);最终都会进入SqlSession例如sqlSession.selectOne(...)再继续往下SqlSession ↓ Executor ↓ StatementHandler ↓ JDBC前面《一条 SQL 在 MyBatis 里到底经历了什么》已经分析过这条调用链。所以SqlSession 是整个 MyBatis 执行流程的入口。为什么它不能共享看看 DefaultSqlSession。源码里面保存了很多运行状态。例如privatefinalConfigurationconfiguration;privatefinalExecutorexecutor;privatebooleandirty;尤其是ExecutorExecutor 里面又维护着一级缓存 事务状态 数据库连接 执行上下文这些都是一次数据库会话的数据。如果两个线程同时共享一个 SqlSession。例如线程 AselectById()线程 BupdateUser()它们可能共用一级缓存共用事务共用 Connection同时修改 Executor 状态整个会话状态都会混乱。所以SqlSession 天生就不能设计成线程安全。如果共享会发生什么假设SqlSessionsqlSessionsqlSessionFactory.openSession();然后ThreadA调用sqlSession.selectList(...)与此同时ThreadB调用sqlSession.commit();这时候线程 A 查询还没结束。线程 B 已经提交事务。甚至关闭了连接。结果可想而知。所以一个 SqlSession只能属于一个线程。那为什么 Mapper 可以共享真正神奇的地方就在这里。Spring 注入的其实不是DefaultSqlSession而是SqlSessionTemplate很多人从来没见过这个类。但它才是 MyBatis 和 Spring 整合最关键的一层。整个调用关系大概是这样Controller │ ▼ MapperJDK 动态代理 │ ▼ SqlSessionTemplate │ ▼ 当前线程 SqlSession │ ▼ Executor │ ▼ JDBC真正共享的是SqlSessionTemplate而不是DefaultSqlSessionSqlSessionTemplate 做了什么每次执行 Mapper 方法。都会进入SqlSessionTemplate然后根据当前线程获取属于自己的 SqlSession。核心逻辑可以理解成SqlSessionsqlSessionSqlSessionUtils.getSqlSession(...);如果当前线程已经存在 SqlSession直接复用。如果没有创建新的 SqlSession。整个过程和线程绑定线程 ASqlSession A线程 BSqlSession B线程 CSqlSession 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一级缓存TransactionConnection这些状态天然不能共享。真正做到线程安全的。不是 SqlSession。而是SqlSessionTemplate ThreadLocal。Spring 共享的是 Mapper。而每个线程真正使用的却始终是属于自己的 SqlSession。这也是为什么我们每天放心注入一个 Mapper。却从来不用担心并发问题。上一篇《为什么很多公司禁用 MyBatis 二级缓存》下一站《Redis 为什么这么快它真的只是因为内存吗》如果这篇文章让你真正理解了Mapper 为什么能单例而 SqlSession 却不能共享欢迎点个赞。你也可以在评论区聊聊你以前是不是一直以为 Mapper 和 SqlSession 是同一个东西