下面是对 MyBatis 一级缓存与二级缓存的源码级深度分析,涵盖其设计原理、核心类、执行流程、生命周期以及常见问题。通过本篇分析,你将彻底理解 MyBatis 缓存机制的本质。
🧩 一、MyBatis 缓存概览
| 缓存类型 | 作用范围 | 默认状态 | 数据结构 | 线程安全 |
|---|---|---|---|---|
| 一级缓存(Local Cache) | SqlSession 级别 | 开启 | HashMap | 否(每个 SqlSession 独享) |
| 二级缓存(Global Cache) | namespace 级别(Mapper 接口) | 关闭(需手动开启) | ConcurrentHashMap + 装饰器 | 是 |
✅ 缓存目标:避免重复 SQL 查询,提升性能。
🔍 二、一级缓存(Local Cache)源码分析
1. 核心类与调用链
// 核心入口
org.apache.ibatis.executor.BaseExecutor
└── protected PerpetualCache localCache; // 一级缓存实例
└── query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler)
2. 执行流程(关键步骤)
3. 缓存 Key 生成:CacheKey
CacheKey是一个复合键,包含:MappedStatement.id(如UserMapper.selectById)environment.id(环境标识)- SQL 语句(带
?占位符) - 参数值(
parameterObject) - 分页信息(
RowBounds) - 其他配置
💡 即使 SQL 相同,参数不同也会生成不同
CacheKey。
4. 缓存失效时机
一级缓存会在以下操作后自动清空:
| 操作 | 源码位置 | 说明 |
|---|---|---|
insert/update/delete | BaseExecutor.clearLocalCache() | 写操作后清除,避免脏读 |
sqlSession.clearCache() | 手动调用 | 清空当前会话缓存 |
sqlSession.close() | 关闭会话 | 缓存生命周期结束 |
📌 源码位置:
BaseExecutor中的update()、commit()、rollback()均会触发清空。
5. 源码关键点
// org.apache.ibatis.executor.BaseExecutor
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
public <E> List<E> query(...) {
// 1. 先查本地缓存
list = localCache.getObject(key);
if (list == null) {
// 2. 缓存未命中,查数据库
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
return list;
}
private <E> List<E> queryFromDatabase(...) {
localCache.putObject(key, EXECUTION_PLACEHOLDER); // 占位符防止重复查询
try {
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key); // 移除占位符
}
localCache.putObject(key, list); // 放入真实结果
localCache.putObject(queryStack, key); // 支持嵌套查询
}
🌐 三、二级缓存(Second Level Cache)源码分析
1. 核心类与架构
// 核心组件
org.apache.ibatis.session.Configuration
└── protected final Map<String, Cache> caches; // 所有 namespace 的缓存
org.apache.ibatis.cache.Cache
└── 实现类:PerpetualCache(基础缓存)
└── 装饰器模式:LruCache, ScheduledCache, SerializedCache, SynchronizedCache 等
org.apache.ibatis.executor.CachingExecutor // 包装原生 Executor
2. 开启方式
<!-- 在 Mapper XML 中开启 -->
<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>
或使用注解:
@CacheNamespace(eviction = LruCache.class, size = 512)
public interface UserMapper {}
3. 执行流程
4. 关键机制:TransactionalCacheManager
- 二级缓存不是直接写入,而是通过
TransactionalCache包装。 - 在事务未提交前,写入的是“临时缓存”。
- 提交时才真正写入二级缓存,避免脏读。
// org.apache.ibatis.executor.CachingExecutor
public void commit(boolean required) throws SQLException {
delegate.commit(required);
tcm.commit(); // 将事务缓存 flush 到二级缓存
}
5. 缓存装饰器(Decorator 模式)
MyBatis 使用装饰器模式增强缓存功能:
| 装饰器 | 作用 |
|---|---|
LruCache | 最近最少使用淘汰策略 |
FifoCache | 先进先出 |
ScheduledCache | 定时刷新 |
SerializedCache | 序列化存储(跨 JVM) |
SynchronizedCache | 线程安全包装 |
配置示例:
<cache type="com.example.CustomCache">
<property name="cacheSize" value="1024"/>
<cache type="org.apache.ibatis.cache.decorators.LruCache"/>
<cache type="org.apache.ibatis.cache.decorators.ScheduledCache">
<property name="interval" value="5000"/>
</cache>
</cache>
6. 源码关键点
// org.apache.ibatis.executor.CachingExecutor
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) {
// 获取缓存(由 Configuration 初始化时注册)
Cache cache = ms.getCache();
if (cache != null) {
CacheKey key = createCacheKey(ms, parameterObject, rowBounds);
// 先查二级缓存
Object value = cache.getObject(key);
if (value == null) {
// 未命中,走原生执行器(会查一级缓存或数据库)
delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// 结果会被 TransactionalCacheManager 缓存,提交时写入二级缓存
}
return (List<E>) value;
}
return delegate.query(...);
}
⚠️ 四、一级缓存 vs 二级缓存对比
| 特性 | 一级缓存 | 二级缓存 |
|---|---|---|
| 作用域 | SqlSession | namespace(Mapper 接口) |
| 默认开启 | 是 | 否(需 <cache> 标签) |
| 线程安全 | 否 | 是(使用 SynchronizedCache 装饰) |
| 跨会话共享 | 否 | 是 |
| 缓存失效 | 写操作、close、clear | 写操作、flushInterval、size 满 |
| 事务支持 | 会话内有效 | 支持事务提交后写入(TransactionalCache) |
| 适用场景 | 单次请求内重复查询 | 跨请求的热点数据(如字典表) |
🧠 五、常见问题与坑点
❌ 问题 1:二级缓存导致脏读
- 原因:多个
SqlSession共享缓存,但事务未隔离。 - 解决方案:
- 设置
readOnly="false"并实现Serializable,避免共享引用。 - 高并发下建议使用 Redis 替代二级缓存。
- 设置
❌ 问题 2:缓存未命中,SQL 多次执行
- 原因:不同
SqlSession不共享一级缓存。 - 验证:
SqlSession s1 = factory.openSession(); s1.getMapper(UserMapper.class).selectById(1); // 执行 SQL s1.close(); SqlSession s2 = factory.openSession(); s2.getMapper(UserMapper.class).selectById(1); // 再次执行 SQL(一级缓存不跨会话)
❌ 问题 3:<cache> 配置不生效
- 原因:
- Mapper 接口没有对应 XML 或注解未启用。
SqlSessionFactory未刷新Configuration。
- 解决:检查 XML 中是否有
<cache>,或使用@CacheNamespace。
✅ 六、最佳实践建议
- 一级缓存:合理使用,避免长会话导致内存泄漏。
- 二级缓存:
- 仅用于读多写少、数据一致性要求不高的场景(如配置表、字典)。
- 避免在高并发写场景使用。
- 推荐用 Redis + MyBatis 二级缓存接口 替代默认缓存。
- 缓存粒度:按
namespace控制,避免缓存污染。 - 序列化:若启用二级缓存,POJO 必须实现
Serializable。
📚 总结:MyBatis 缓存本质
| 维度 | 说明 |
|---|---|
| 一级缓存本质 | SqlSession 内的 HashMap,防止同会话重复查询 |
| 二级缓存本质 | namespace 级的共享缓存,基于装饰器模式 + 事务缓存 |
| 设计思想 | 简单有效,不追求复杂分布式缓存,留出扩展空间 |
| 源码启示 | 学习了 CacheKey 的设计、装饰器模式、事务缓存管理 |

被折叠的 条评论
为什么被折叠?



