本文作者: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 | 负责JDBC Statement创建、参数赋值、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 / ROWNUM
SQL执行完成后自动清除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 核心对比
| 会话管理 | 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底层原理与自研框架实践



