彻底吃透MyBatis核心原理:SqlSession、两级缓存、Spring集成机制一次说清吃透
本文作者:CodeStats
一个专注分享Java底层原理、自研框架实战干货的技术博主
如果觉得内容实用,欢迎点赞 + 收藏 + 关注!
📖 目录
一、写在前面
二、SqlSession:MyBatis的核心入口
三、生命周期:非线程安全,禁止共享
四、两级缓存机制(高频面试+生产重点)
五、核心组件与完整执行流程
六、分页机制:为什么不用RowBounds?
七、Spring集成原理:SqlSessionTemplate的秘密
八、自研框架模拟MyBatis事务
九、核心速查表
十、写在最后
一、写在前面
日常开发中,我们几乎每天都会用到MyBatis。下面这段基础代码,想必所有Java开发者都见过:
java
SqlSession sqlSession = sqlSessionFactory.openSession(); try { UserMapper mapper = sqlSession.getMapper(UserMapper.class); User user = mapper.selectById(1); sqlSession.commit(); } finally { sqlSession.close(); }很多人只会机械套用这段代码,但很少有人深究:
SqlSession的本质是什么?它的生命周期到底该如何把控?
事务提交、会话关闭的底层意义是什么?
本文将从底层逻辑出发,逐一拆解MyBatis最核心的SqlSession会话、两级缓存、Spring集成机制,帮你彻底吃透MyBatis核心原理,告别只会用、不懂原理的开发困境。
二、SqlSession:MyBatis的核心入口
用最通俗的话总结:
SqlSession是MyBatis与数据库交互的一次性工作单元,也是MyBatis操作数据库的核心入口。
2.1 SqlSession的四大核心职责
| 职责 | 说明 |
|---|---|
| 1️⃣ 执行SQL语句 | 查询、新增、修改、删除 |
| 2️⃣ 获取Mapper代理对象 | 通过动态代理返回接口实现 |
| 3️⃣ 管理数据库事务 | 开启、提交、回滚、关闭 |
| 4️⃣ 维护一级缓存 | 会话级别的本地缓存 |
三、生命周期:非线程安全,禁止共享
3.1 核心原则(⭐最重要)
SqlSession不具备线程安全性,多个线程不能共用同一个SqlSession。
违反这一原则会导致:数据错乱 + 事务异常 + 诡异Bug
3.2 标准使用规范
| 场景 | 使用策略 |
|---|---|
| 单次数据库操作 | 单独创建一个SqlSession |
| Web项目 | 一个HTTP请求对应一个独立SqlSession |
| 多线程场景 | 每个线程持有自己的SqlSession,互不干扰 |
3.3 完整生命周期流程图
text
应用启动 ↓ SqlSessionFactory(全局单例,只创建一次) ↓ 每次数据库操作 → openSession() → 创建新的SqlSession ↓ 获取Mapper代理对象 → 执行SQL ↓ commit / rollback ↓ close()(必须!)
3.4 ✅ 正确 vs ❌ 错误示例
✅ 正确写法:try-with-resources(推荐)
java
// 自动关闭会话,无需手动finally try (SqlSession session = sqlSessionFactory.openSession()) { UserMapper mapper = session.getMapper(UserMapper.class); return mapper.selectById(id); }❌ 错误写法:会话未关闭(连接泄漏)
java
// 高危:会话未关闭,长期堆积会导致连接池耗尽 SqlSession session = sqlSessionFactory.openSession(); UserMapper mapper = session.getMapper(UserMapper.class); return mapper.selectById(id); // 无close()操作,资源无法释放
⚠️后果:数据库连接池耗尽 → 应用假死 → 线上事故
四、两级缓存机制(高频面试+生产重点)
MyBatis内置两级缓存,用于减少数据库查询次数、提升接口性能。
4.1 一级缓存(SqlSession级别)
核心特性:
| 属性 | 说明 |
|---|---|
| 默认状态 | ✅ 默认开启,无法关闭 |
| 作用范围 | 仅当前同一个SqlSession内有效 |
| 底层实现 | PerpetualCache,本质是HashMap |
| 生命周期 | 会话创建 → 缓存创建;会话关闭 → 缓存销毁 |
演示案例
java
try (SqlSession session = factory.openSession()) { UserMapper mapper = session.getMapper(UserMapper.class); User user1 = mapper.selectById(1); // 第一次查询,访问数据库 User user2 = mapper.selectById(1); // 第二次查询,命中一级缓存 System.out.println(user1 == user2); // 输出 true(同一内存地址) }❌ 一级缓存失效场景(重点)
| 场景 | 说明 |
|---|---|
| 执行增删改操作 | INSERT/UPDATE/DELETE |
| 手动清空缓存 | 调用clearCache() |
| 事务提交/回滚 | commit()/rollback() |
| 会话关闭 | close() |
4.2 二级缓存(Mapper级别)
核心特性:
| 属性 | 说明 |
|---|---|
| 默认状态 | ❌ 默认关闭,需手动开启 |
| 作用范围 | 全局同一个Mapper接口下,跨会话共享 |
| 数据持久 | 会话关闭后缓存依然存在(应用级别) |
| 实体类要求 | 必须实现Serializable序列化接口 |
开启二级缓存(两步)
第一步:全局配置文件开启总开关
xml
<!-- mybatis-config.xml --> <settings> <setting name="cacheEnabled" value="true"/> </settings>
第二步:Mapper.xml开启缓存
xml
<mapper namespace="com.example.UserMapper"> <cache eviction="LRU" <!-- 淘汰策略:最近最少使用 --> flushInterval="60000" <!-- 60秒刷新一次 --> size="512" <!-- 最多缓存512个对象 --> readOnly="false"/> <!-- 非只读,可返回克隆对象 --> </mapper>
4.3 两级缓存核心对比
| 对比项 | 一级缓存 | 二级缓存 |
|---|---|---|
| 作用范围 | 单个SqlSession会话 | 跨SqlSession,全局Mapper级别 |
| 默认状态 | ✅ 开启(不可关闭) | ❌ 关闭(需手动开启) |
| 生命周期 | 会话结束即销毁 | 应用运行期间持续存在 |
| 共享性 | 线程/会话私有 | 全局共享 |
| 实体类要求 | 无要求 | 必须实现Serializable |
五、核心组件与完整执行流程
5.1 核心组件职责一览
| 组件 | 职责 |
|---|---|
SqlSessionFactory | 会话工厂,全局单例,唯一职责:创建SqlSession |
SqlSession | 数据库会话,SQL执行 + 事务管理 + 缓存维护 |
Executor | SQL执行器,负责缓存校验、SQL执行、事务调度 |
MappedStatement | 封装SQL语句、参数规则、结果映射规则 |
StatementHandler | 负责JDBCStatement创建、参数赋值、SQL执行 |
ParameterHandler | 处理SQL参数的类型转换与赋值 |
ResultSetHandler | 处理结果集,封装为Java实体对象 |
TypeHandler | 实现Java类型 ↔ 数据库字段类型转换 |
5.2 完整组件交互流程图
text
开发者调用 Mapper 接口方法 ↓ 获取 Mapper 动态代理对象(JDK代理) ↓ 代理对象调用 SqlSession 查询/更新方法 ↓ ┌─────────────────────────────────────┐ │ Executor 执行器 │ │ ① 校验二级缓存 → ② 校验一级缓存 │ └─────────────────────────────────────┘ ↓(缓存未命中) StatementHandler 执行真实SQL ↓ ParameterHandler 注入参数 ↓ JDBC 执行 SQL ↓ ResultSetHandler 封装结果集 ↓ 结果存入缓存 → 返回给开发者
六、分页机制:为什么不用RowBounds?
6.1 原生RowBounds(❌ 不推荐)
问题本质:内存分页,不会修改原生SQL
java
// 不推荐:会查询全量数据后在内存中截取 RowBounds rowBounds = new RowBounds(offset, limit); List<User> list = session.selectList("selectAll", null, rowBounds);⚠️严重缺陷:数据量大时 → 全量查库 → 内存溢出 → 性能卡顿
6.2 PageHelper物理分页(✅ 生产推荐)
核心原理:基于MyBatis插件机制,直接改写底层SQL
java
// 推荐:仅查询当前页数据 PageHelper.startPage(pageNum, pageSize); List<User> list = userMapper.selectAll(); PageInfo<User> pageInfo = new PageInfo<>(list);
SQL改写效果对比:
sql
-- 原始SQL SELECT * FROM user -- PageHelper改写后(MySQL) SELECT * FROM user LIMIT 0, 10
PageHelper核心原理(三步):
通过
ThreadLocal获取当前线程的分页参数根据数据库类型动态拼接
LIMIT/ROWNUMSQL执行完成后自动清除
ThreadLocal,避免参数污染
七、Spring集成原理:SqlSessionTemplate的秘密
7.1 核心类:SqlSessionTemplate
Spring并未直接使用原生
SqlSession,而是通过SqlSessionTemplate封装,实现会话自动管理 + 事务同步 + 线程安全适配。
SqlSessionTemplate本质是SqlSession的JDK动态代理对象。
7.2 对象依赖链路(Spring容器启动时)
text
SqlSessionFactoryBean(配置加载) ↓ SqlSessionFactory(全局单例) ↓ MapperFactoryBean(为每个Mapper接口生成) ↓ Mapper代理对象(JDK动态代理) ↓ SqlSessionTemplate(代理原生SqlSession) ↓ DefaultSqlSession(原生会话) ↓ Executor执行器
7.3 日常开发:直接@Autowired注入
配置类:
java
@Configuration @MapperScan("com.example.mapper") // 扫描Mapper接口包 public class MyBatisConfig { @Bean public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); factoryBean.setDataSource(dataSource); return factoryBean.getObject(); } }业务使用:
java
@Service public class UserService { @Autowired private UserMapper userMapper; // Spring自动注入代理对象 public User getUser(Integer id) { return userMapper.selectById(id); // 无需手动创建会话 } }自动注入核心流程:
@MapperScan扫描指定包下所有Mapper接口为每个接口生成
MapperFactoryBean工厂Bean工厂Bean通过
SqlSessionTemplate获取JDK动态代理对象代理对象交由Spring容器管理,实现自动注入
八、自研框架模拟MyBatis事务
为了更透彻理解MyBatis事务与代理原理,我们通过自研框架模拟声明式事务的实现逻辑。
8.1 自定义事务注解
java
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Transactional { Propagation propagation() default Propagation.REQUIRED; }8.2 事务动态代理实现
java
public class TransactionProxy implements InvocationHandler { private final Object target; private final JdbcTemplate jdbcTemplate; @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 判断方法是否添加事务注解 if (method.isAnnotationPresent(Transactional.class)) { try { jdbcTemplate.beginTransaction(); // 开启事务 Object result = method.invoke(target, args); jdbcTemplate.commit(); // 提交事务 return result; } catch (Exception e) { jdbcTemplate.rollback(); // 回滚事务 throw e; } } // 无事务注解,直接执行 return method.invoke(target, args); } }8.3 事务功能使用示例
java
@Service public class UserService { @Autowired private UserMapper userMapper; @Transactional // 添加事务注解,方法内所有操作纳入同一事务 public void transfer(Long fromId, Long toId, BigDecimal amount) { userMapper.decreaseBalance(fromId, amount); userMapper.increaseBalance(toId, amount); } }8.4 自研框架 vs MyBatis 核心对比
| 核心特性 | 原生MyBatis | CodeStats自研框架 |
|---|---|---|
| 会话管理 | SqlSession | JdbcTemplate+ 动态代理 |
| 事务控制 | 手动commit/rollback + Spring声明式 | 自定义@Transactional注解 |
| Mapper代理 | JDK动态代理 | JDK动态代理 |
| 连接池 | 适配HikariCP等 | 自研SimpleDataSource |
| SQL执行 | StatementHandler处理器 | 直接基于JdbcTemplate |
九、核心速查表
| 知识点 | 核心结论 |
|---|---|
| SqlSession | 非线程安全,单请求单会话,用完必须close() |
| 一级缓存 | 默认开启、会话级别HashMap,增删改/提交后自动清空 |
| 二级缓存 | 手动开启、Mapper全局共享,实体类需实现Serializable |
| 分页机制 | 摒弃RowBounds,生产统一用PageHelper物理分页 |
| Spring集成 | SqlSessionTemplate代理会话,自动管理生命周期 |
| 事务原理 | 基于动态代理 + 注解,实现声明式事务 |
十、写在最后
MyBatis作为Java生态主流的ORM框架,熟练使用只是基础。
掌握底层的会话机制、缓存原理、Spring集成逻辑,才能:
✅ 规避缓存失效、事务异常、连接泄漏等常见问题
✅ 写出更健壮、更高效的数据库访问代码
✅ 在面试中轻松应对底层原理追问
✅ 迈出高级开发工程师的关键一步
💬 互动话题
你在开发中遇到过哪些因为不懂MyBatis底层原理而踩的坑?
一级缓存导致的“数据读不到”?
二级缓存序列化报错?
还是事务不回滚的诡异Bug?
欢迎在评论区分享你的经历,一起交流学习、共同进步!
📌 如果本文对你有帮助,欢迎:
👍点赞支持原创
⭐收藏方便回看
🔁转发给需要的朋友
👀关注CodeStats,陪你深度进阶技术!
CodeStats—— 专注Java底层原理与自研框架实践
