MyBatis 的 SqlSession 是什么?它的生命周期和线程安全性是怎样的?
简化版
**SqlSession 是 MyBatis 里「执行 SQL 的会话」——你通过它执行增删改查、提交/回滚事务、拿 Mapper 代理。它是 MyBatis 面向应用的核心接口。**理解它的关键是「生命周期和线程安全」:① SqlSessionFactory(用来创建 SqlSession 的工厂)——线程安全、全局单例、应用生命周期(创建一次,一直用,别反复创建);② SqlSession——线程不安全、生命周期极短(一次请求/一个方法/一个事务)、用完必须关闭,绝对不能多个线程共享一个 SqlSession,也不能把它作为成员变量长期持有。为什么 SqlSession 线程不安全:它内部有一级缓存、有当前事务/连接状态,多线程共用会互相污染、数据错乱。在 Spring 里怎么办:整合 mybatis-spring 后,你不用手动 openSession()/close()——Spring 用 SqlSessionTemplate(线程安全的 SqlSession 代理)+ 事务同步,自动为每个线程/事务管理一个 SqlSession,用完自动关,Mapper 注入进来直接用就行。
详细版
三个核心对象的生命周期与线程安全:
| 对象 | 作用 | 线程安全 | 生命周期 | 数量 |
|---|---|---|---|---|
| SqlSessionFactoryBuilder | 建 Factory 的构建器 | — | 方法内(用完丢弃) | 临时 |
| SqlSessionFactory | 创建 SqlSession 的工厂 | ✅ 安全 | 应用级(全局) | 单例 |
| SqlSession | 执行 SQL 的会话 | ❌ 不安全 | 请求/方法/事务级 | 每次新建、用完关 |
| Mapper 实例 | 接口的代理 | 取决于 SqlSession | 方法级 | 每次获取 |
// 原生 MyBatis 用法(手动管理 SqlSession)
SqlSessionFactory factory = ...; // 全局单例,创建一次
// 每次操作:开一个 SqlSession,用完关闭(try-with-resources)
try (SqlSession session = factory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
User user = mapper.selectById(1);
session.commit(); // 提交事务(默认 openSession 不自动提交)
} // 自动 close,释放连接、清一级缓存
// Spring 整合后(不用手动管理)
@Autowired private UserMapper userMapper; // 直接注入用,Spring 管理 SqlSession
⚠️ 「SqlSession 线程不安全、不能共享」是 MyBatis 使用的第一红线——它不是无状态的工具类,而是有状态的会话对象:内部持有当前的一级缓存、当前数据库连接、当前事务状态。如果把 SqlSession 做成 Controller/Service 的成员变量(一个实例被多线程共用),或者多个线程传递同一个 SqlSession,就会出现「A 线程的事务提交了 B 线程的数据」「一级缓存读到别的线程的结果」「连接状态错乱」等灾难性问题。正确姿势:原生 MyBatis 里,SqlSession 是「方法内的局部变量」,
try-with-resources用完即关;Spring 里则完全交给SqlSessionTemplate托管,你只管注入 Mapper 用——永远不要长期持有或跨线程共享 SqlSession。
完整版教学
一、SqlSession 是什么
SqlSession 是 MyBatis 面向应用的「执行 SQL 的会话」:
SqlSession 提供的能力:
- 执行 SQL:selectOne/selectList/insert/update/delete
- 事务控制:commit()/rollback()
- 获取 Mapper:getMapper(XxxMapper.class) → 拿到接口的代理
- 管理一级缓存、清缓存 clearCache()、关闭 close()
它是 MyBatis 的"门面":
你和 MyBatis 打交道,主要就是通过 SqlSession(或它给你的 Mapper 代理)
底层的 Executor、StatementHandler、连接 都被它封装了
一次会话的含义:
一个 SqlSession 代表"一次和数据库的会话"
期间:用一个数据库连接、维护一个一级缓存、在一个事务里
→ 所以它是"有状态"的、生命周期短的
SqlSession 是 MyBatis 面向应用的「执行 SQL 的会话门面」——提供执行 SQL(selectList/insert 等)、事务控制(commit/rollback)、获取 Mapper 代理、管理一级缓存等能力。它封装了底层的 Executor、连接等。一个 SqlSession 代表「一次和数据库的会话」(用一个连接、维护一个一级缓存、在一个事务里),所以它有状态、生命周期短。理解「SqlSession 是执行 SQL 的会话门面(执行 SQL/事务/getMapper/一级缓存)、代表一次数据库会话、有状态生命周期短」,就理解了它的定位。
二、SqlSessionFactory:全局单例的工厂
SqlSessionFactory 是「创建 SqlSession 的工厂」,和 SqlSession 的生命周期截然相反:
SqlSessionFactory:
职责:openSession() 创建 SqlSession
内部持有:Configuration(全局配置——所有 Mapper、设置、类型处理器等)
生命周期和线程安全:
✅ 线程安全(无状态,只是个创建 SqlSession 的工厂)
✅ 应用级生命周期——应用启动时创建一次,全程复用
✅ 全局单例——一个数据库对应一个 SqlSessionFactory
为什么单例:
创建 SqlSessionFactory 开销大(要解析配置、Mapper XML)
它又是线程安全的、可复用的
→ 创建一次、全局共享,别反复创建(反复创建=浪费+可能资源泄漏)
对比 SqlSession:
Factory:线程安全、长寿命、单例
Session:线程不安全、短寿命、每次新建
→ 一个"生产者"(长寿命工厂)造一堆"短命产品"(会话)
SqlSessionFactory 是「创建 SqlSession 的工厂」——内部持有 Configuration(全局配置)。它和 SqlSession 生命周期截然相反:线程安全、应用级生命周期、全局单例(创建一次全程复用)。因为创建它开销大(解析配置和 Mapper XML)、又线程安全可复用,所以「创建一次、全局共享,别反复创建」。理解「SqlSessionFactory 是创建 SqlSession 的工厂、持有 Configuration、线程安全+应用级+单例(创建一次复用)、和 SqlSession 生命周期相反」,就理解了工厂的定位。
三、为什么 SqlSession 线程不安全
关键要理解「SqlSession 为什么不能共享/跨线程」:
SqlSession 是"有状态"的,它持有:
① 一级缓存(本地缓存):
缓存本次会话查过的结果(同 SqlSession 内相同查询走缓存)
→ 多线程共用 → A 线程缓存的结果被 B 线程读到 → 数据错乱
② 当前数据库连接:
一个 SqlSession 绑定一个连接
→ 多线程用同一连接 → 连接不是线程安全的 → 崩
③ 当前事务状态:
SqlSession 维护当前事务(未提交的操作)
→ 多线程共用 → A 的 commit 把 B 的操作也提交了 → 事务混乱
结论:SqlSession = 有状态的会话对象 ≠ 无状态工具类
多线程共享一个 SqlSession = 缓存污染 + 连接错乱 + 事务混乱
→ 绝对不能共享、不能作为成员变量长期持有
正确:每个线程/请求/事务,用自己独立的 SqlSession
SqlSession 线程不安全,是因为它「有状态」——持有 ① 一级缓存(多线程共用会读到别人的结果)、② 当前数据库连接(连接非线程安全)、③ 当前事务状态(多线程共用会导致 A 的 commit 提交 B 的操作)。所以 SqlSession 是有状态的会话对象,不是无状态工具类,多线程共享 = 缓存污染 + 连接错乱 + 事务混乱。正确做法是每个线程/请求/事务用独立的 SqlSession。理解「SqlSession 线程不安全因有状态(一级缓存/当前连接/当前事务)、共享导致缓存污染+连接错乱+事务混乱、每线程用独立 SqlSession」,就理解了这条第一红线的原理。
四、SqlSession 的正确使用:短生命周期
SqlSession 的正确用法是「短生命周期、用完即关」:
原生 MyBatis 的正确用法:
try (SqlSession session = factory.openSession()) {
// 在这个方法/请求范围内使用
UserMapper mapper = session.getMapper(UserMapper.class);
mapper.xxx();
session.commit(); // 手动提交(openSession() 默认不自动提交)
} // try-with-resources 自动 close
close() 做什么:
- 归还数据库连接(到连接池)
- 清空一级缓存
- 结束会话
→ 不 close 会导致连接泄漏(连接不还池,池被耗尽)
范围建议:
一个 SqlSession 的范围 = 一次请求 / 一个业务方法 / 一个事务
用完立刻关,不要跨请求、不要长期持有
事务:
openSession():默认手动提交(要自己 commit)
openSession(true):自动提交(每条 SQL 自动 commit,通常不用)
SqlSession 的正确用法是「短生命周期、用完即关」——用 try-with-resources 保证 close()(归还连接到池、清一级缓存;不 close 会连接泄漏耗尽连接池)。范围应是「一次请求/一个业务方法/一个事务」,不跨请求、不长期持有。openSession() 默认手动提交(要自己 commit),openSession(true) 自动提交。理解「SqlSession 短生命周期用完即关(try-with-resources,不 close 会连接泄漏)、范围是一次请求/方法/事务、openSession 默认手动提交」,就掌握了正确用法。
五、Spring 整合:SqlSessionTemplate 托管
在 Spring 里,SqlSession 的管理被 mybatis-spring 接管,不用手动 open/close:
问题:手动 openSession/commit/close 麻烦、易错(忘了 close 就泄漏)
且要和 Spring 的事务管理配合
mybatis-spring 的方案:SqlSessionTemplate
它是一个"线程安全的 SqlSession 代理"(实现了 SqlSession 接口)
内部机制:
- 每次操作时,通过 Spring 的事务同步(TransactionSynchronizationManager)
拿到"当前线程/当前事务"绑定的那个真实 SqlSession
- 如果在 Spring 事务里,用事务绑定的 SqlSession(保证同事务用同一个)
- 操作完,SqlSession 的提交/关闭 交给 Spring 事务管理器统一处理
所以在 Spring 里:
@Autowired UserMapper userMapper; // Mapper 底层用 SqlSessionTemplate
userMapper.selectById(1); // 直接用,不用管 SqlSession
→ SqlSessionTemplate 自动为每个线程/事务管理正确的 SqlSession
→ 线程安全(每个线程拿到自己的)、自动提交/回滚(跟随 Spring 事务)、自动关闭
★ 这就是为什么 Spring 里你从不手动 openSession/close
Spring 里 SqlSession 由 SqlSessionTemplate(mybatis-spring 提供) 托管——它是「线程安全的 SqlSession 代理」:每次操作通过 Spring 的事务同步拿到「当前线程/当前事务绑定的真实 SqlSession」(保证同事务用同一个),提交/关闭交给 Spring 事务管理器。所以你只管注入 Mapper 直接用,不用手动 open/close——SqlSessionTemplate 自动为每个线程/事务管理正确的 SqlSession(线程安全、跟随 Spring 事务提交回滚、自动关闭)。理解「Spring 里 SqlSessionTemplate 托管 SqlSession(线程安全代理)、通过事务同步拿当前事务绑定的 SqlSession、提交关闭交给 Spring 事务、所以只管注入 Mapper 用」,就理解了 Spring 整合下的管理。
六、Mapper 的线程安全与整体总结
最后理解 Mapper 的线程安全,并总结整体:
Mapper 实例的线程安全:
Mapper 是接口的动态代理,代理背后是 SqlSession
- 原生 MyBatis:session.getMapper() 拿到的 Mapper 绑定那个 SqlSession
→ 它的线程安全性 = 那个 SqlSession 的(不安全,随 SqlSession 短命)
- Spring 里:注入的 Mapper 底层是 SqlSessionTemplate
→ 线程安全(每次调用拿当前线程的 SqlSession)→ 可以做成单例注入到处用
整体生命周期链(记忆):
SqlSessionFactoryBuilder(临时,建完 Factory 就丢)
→ SqlSessionFactory(应用级、单例、线程安全)
→ SqlSession(请求/方法/事务级、每次新建、线程不安全、用完关)
→ Mapper(方法级,随 SqlSession)
一句话总结生命周期:
Factory 长寿命单例,Session 短寿命每次新建,Spring 里 Session 交给托管
Mapper 的线程安全取决于底层 SqlSession:原生 MyBatis 里 Mapper 绑定某个 SqlSession(不安全、短命);Spring 里注入的 Mapper 底层是 SqlSessionTemplate(线程安全,可单例注入到处用)。整体生命周期链:SqlSessionFactoryBuilder(临时)→ SqlSessionFactory(应用级单例、线程安全)→ SqlSession(请求/方法/事务级、每次新建、不安全、用完关)→ Mapper(方法级)。理解「Mapper 线程安全取决于底层 SqlSession(Spring 里 SqlSessionTemplate 线程安全可单例)、生命周期链 Builder(临时)→Factory(单例)→Session(短命)→Mapper」,就掌握了整体的生命周期图景。
记忆钩子:「SqlSession=执行 SQL 的会话门面(执行 SQL/commit/rollback/getMapper/一级缓存);生命周期:SqlSessionFactoryBuilder(临时)→SqlSessionFactory(应用级/单例/线程安全,创建一次复用)→SqlSession(请求/方法/事务级、每次新建、★线程不安全、用完必须关);SqlSession 线程不安全因有状态(一级缓存/当前连接/当前事务)→共享导致缓存污染+连接错乱+事务混乱,绝不跨线程共享或长期持有;原生用 try-with-resources 用完即关(不关连接泄漏);Spring 里 SqlSessionTemplate 托管(线程安全代理,按事务同步拿当前 SqlSession),只管注入 Mapper 用」。
七、常见误区与追问
- 误区:SqlSession 是线程安全的、可以共享。 绝对不是——SqlSession 有状态(一级缓存、当前连接、当前事务),多线程共享会导致缓存污染、连接错乱、事务混乱;它是短生命周期的局部对象,不能作为成员变量长期持有或跨线程共享。
- 误区:SqlSessionFactory 每次用都要新建。 恰恰相反——SqlSessionFactory 线程安全、应用级、全局单例,创建一次全程复用;反复创建它开销大(要解析配置和 Mapper XML)且可能资源泄漏。
- 误区:SqlSession 用完不用关也没事。 必须关——close() 会归还数据库连接到池、清空一级缓存;不 close 会导致连接泄漏,连接不还池、最终连接池耗尽,应用无法获取新连接;用 try-with-resources 保证关闭。
- 误区:Spring 里也要手动 openSession/close。 不用——mybatis-spring 的 SqlSessionTemplate 托管了 SqlSession,通过 Spring 事务同步自动为每个线程/事务管理正确的 SqlSession、跟随事务提交回滚、自动关闭;你只管注入 Mapper 直接用。
- 追问:为什么 Spring 里注入的 Mapper 可以做成单例到处用,而原生的不行? Spring 里 Mapper 底层是 SqlSessionTemplate(线程安全代理)——每次调用时通过事务同步拿到「当前线程/事务绑定的 SqlSession」,所以单例的 Mapper 被多线程调用也各用各的 SqlSession,线程安全;原生 MyBatis 的 Mapper 绑定一个具体 SqlSession(不安全、短命),不能共享。
- 追问:一个 SqlSession 的合理范围是多大? 一次请求 / 一个业务方法 / 一个事务——在这个范围内用,用完立刻关;不要跨请求、不要长期持有;Spring 里一般对应一个事务(同一事务内的多次 Mapper 调用复用同一个 SqlSession,享受一级缓存且事务一致)。
- 追问:SqlSessionFactory 里的 Configuration 是什么? 是 MyBatis 的全局配置对象——包含所有的设置(settings)、类型别名/处理器、所有 Mapper 的 MappedStatement、插件等;SqlSessionFactory 持有它,创建的每个 SqlSession 都共享这份全局配置。
八、加强记忆
SqlSession 是 MyBatis「执行 SQL 的会话门面」——执行增删改查、commit/rollback、getMapper、管理一级缓存。理解它的关键是生命周期和线程安全:① SqlSessionFactory(创建 SqlSession 的工厂,持有全局 Configuration)——线程安全、应用级、全局单例(创建开销大但可复用,创建一次全程共享,别反复创建);② SqlSession——线程不安全、生命周期极短(请求/方法/事务级)、每次新建、用完必须 close(不关会连接泄漏耗尽连接池)。SqlSession 线程不安全的原因是它有状态:持有一级缓存、当前数据库连接、当前事务状态——多线程共享会导致缓存污染 + 连接错乱 + 事务混乱,绝不能跨线程共享或作为成员变量长期持有。原生用法:try-with-resources 用完即关,范围是一次请求/方法/事务。Spring 整合后:mybatis-spring 用 SqlSessionTemplate(线程安全的 SqlSession 代理) 托管——通过 Spring 事务同步为每个线程/事务拿到正确的 SqlSession、跟随事务提交回滚、自动关闭,所以你只管注入 Mapper 直接用(Mapper 因此可单例到处注入)。生命周期链:Builder(临时)→Factory(单例)→Session(短命)→Mapper(方法级)。一句话「SqlSession=有状态的 SQL 会话(线程不安全、短命、用完关),SqlSessionFactory=线程安全的全局单例工厂;SqlSession 有一级缓存/连接/事务状态所以不能跨线程共享;原生 try-with-resources 用完即关,Spring 里 SqlSessionTemplate 按事务托管、只管注入 Mapper 用」。