深入剖析MyBatis缓存机制,从架构设计到实战应用,助你全面掌握缓存优化
一、初识MyBatis缓存
在正式开始之前,让我们先来了解MyBatis的整体架构。MyBatis采用分层设计,而缓存模块作为基础支撑层的核心组件,承担着提升查询性能的重要使命。

缓存的价值何在?
想象这样一个场景:你的系统每秒需要查询1000次用户信息。
无缓存时: 1000次数据库查询/秒
有缓存时: 1次数据库查询 + 999次内存读取/秒
性能提升: 近1000倍!
MyBatis的两级防线
MyBatis提供了两级缓存机制,就像双重保险:
| 缓存类型 | 作用范围 | 生命周期 | 是否默认开启 |
|---|---|---|---|
| 一级缓存 | SqlSession会话 | 与会话同生共死 | 默认开启 |
| 二级缓存 | Mapper命名空间 | 应用级别,跨会话 | 需手动配置 |
二、缓存架构
MyBatis的缓存设计堪称教科书级别的装饰器模式应用。

Cache接口:缓存的灵魂
所有缓存实现都遵循这个核心接口:
csharp
public interface Cache {
String getId(); // 缓存标识
void putObject(Object key, Object value); // 存入缓存
Object getObject(Object key); // 获取缓存
Object removeObject(Object key); // 移除缓存
void clear(); // 清空缓存
int getSize(); // 缓存大小
ReadWriteLock getReadWriteLock(); // 读写锁
}
装饰器大家族
MyBatis通过装饰器模式为缓存"穿衣服",每个装饰器赋予缓存一种新能力:
装饰器功能速查表
| 装饰器 | 核心功能 | 适用场景 |
|---|---|---|
| LruCache | 最近最少使用淘汰 | 热点数据缓存 |
| FifoCache | 先进先出淘汰 | 时序数据缓存 |
| SoftCache | 软引用,内存紧张时回收 | 大对象缓存 |
| WeakCache | 弱引用,GC时回收 | 内存敏感场景 |
| ScheduledCache | 定时清理 | 时效性数据 |
| SerializedCache | 序列化存储 | 数据隔离保护 |
| LoggingCache | 日志记录 | 性能监控 |
| SynchronizedCache | 线程同步 | 并发安全 |
| BlockingCache | 阻塞控制 | 防止击穿 |
| TransactionalCache | 事务缓存 | 事务一致性 |
PerpetualCache:万丈高楼平地起
这是最基础的缓存实现,简单而高效:
typescript
public class PerpetualCache implements Cache {
private final String id;
private final Map<Object, Object> cache = new HashMap<>();
@Override
public void putObject(Object key, Object value) {
cache.put(key, value);
}
@Override
public Object getObject(Object key) {
return cache.get(key);
}
// ... 其他方法实现
}
三、一级缓存:会话的专属记忆
一级缓存是SqlSession级别的缓存,默认开启,无需配置。

工作原理解析
一级缓存的核心逻辑在BaseExecutor中实现:
scss
public <E> List<E> query(MappedStatement ms, Object parameter,
RowBounds rowBounds, ResultHandler resultHandler) {
//创建缓存键
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
//先查缓存
List<E> list = (List<E>) localCache.getObject(key);
if (list != null) {
return list; // 缓存命中!
}
//缓存未命中,查询数据库
list = queryFromDatabase(ms, parameter, rowBounds,
resultHandler, key, boundSql);
//结果写入缓存
localCache.putObject(key, list);
return list;
}
CacheKey:缓存的身份证
CacheKey由多个元素组成,确保唯一性:
sql
CacheKey的组成:
├── MappedStatement的ID
├── 查询参数
├── 分页信息(RowBounds)
├── SQL语句
└── 环境ID
判断缓存命中时,这些元素必须完全一致:
typescript
@Override
public boolean equals(Object object) {
final CacheKey cacheKey = (CacheKey) object;
return hashcode == cacheKey.hashcode // 哈希码相同
&& checksum == cacheKey.checksum // 校验和相同
&& count == cacheKey.count // 元素数量相同
&& updateList.equals(cacheKey.updateList); // 元素列表相同
}
五种失效场景
一级缓存在以下情况会被清空:
- 执行增删改操作
- 手动清空
- 提交事务
- 回滚事务
- 关闭会话
scss
@Override
public int update(MappedStatement ms, Object parameter) {
clearLocalCache(); //清空缓存
return doUpdate(ms, parameter);
}
scss
session.clearCache(); //主动清理
scss
session.commit(); //提交时清空
scss
session.rollback(); //回滚时清空
go
session.close(); //会话结束,缓存消失
实战演示
ini
SqlSession session = sqlSessionFactory.openSession();
UserMapper mapper = session.getMapper(UserMapper.class);
// 第一次查询 - 访问数据库
User user1 = mapper.selectById(1L);
System.out.println("首次查询: " + user1);
// 第二次查询 - 从缓存获取
User user2 = mapper.selectById(1L);
System.out.println("再次查询: " + user2);
// 验证是否为同一对象
System.out.println("同一对象? " + (user1 == user2)); //true
session.close();
四、二级缓存:跨会话的共享空间
二级缓存是Mapper级别的缓存,可以在不同SqlSession之间共享数据。

开启二级缓存
在Mapper XML中添加配置:
xml
<mapper namespace="com.example.mapper.UserMapper">
<!-- 开启二级缓存 -->
<cache eviction="LRU"
flushInterval="60000"
size="1024"
readOnly="true"/>
<select id="selectById" resultType="User">
SELECT * FROM t_user WHERE id = #{id}
</select>
</mapper>
配置参数详解:
| 参数 | 说明 | 可选值 | 默认值 |
|---|---|---|---|
| eviction | 淘汰策略 | LRU/FIFO/SOFT/WEAK | LRU |
| flushInterval | 刷新间隔(毫秒) | 任意正整数 | 不刷新 |
| size | 缓存容量 | 任意正整数 | 1024 |
| readOnly | 是否只读 | true/false | false |
| blocking | 是否阻塞 | true/false | false |
四大淘汰策略
- LRU(推荐)
- FIFO
- SOFT
- WEAK
ini
<cache eviction="LRU"/>
最近最少使用,淘汰最久未访问的数据
ini
<cache eviction="FIFO"/>
先进先出,按写入顺序淘汰
ini
<cache eviction="SOFT"/>
软引用,内存不足时才回收
ini
<cache eviction="WEAK"/>
弱引用,GC时即可回收
CachingExecutor:二级缓存的指挥官
java
public class CachingExecutor implements Executor {
private final TransactionalCacheManager tcm =
new TransactionalCacheManager();
@Override
public <E> List<E> query(...) {
Cache cache = ms.getCache();
if (cache != null) {
//尝试从二级缓存获取
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list != null) {
return list; //命中!
}
}
//委托给BaseExecutor查询(会走一级缓存)
List<E> list = delegate.query(...);
//结果写入二级缓存
if (cache != null) {
tcm.putObject(cache, key, list);
}
return list;
}
}
TransactionalCache:事务缓存管家
事务缓存确保只有提交后的数据才会进入二级缓存:
typescript
public class TransactionalCache implements Cache {
private final Map<Object, Object> entriesToAddOnCommit;
@Override
public void putObject(Object key, Object value) {
//暂存,不立即写入
entriesToAddOnCommit.put(key, value);
}
public void commit() {
// 提交时才真正写入缓存
for (Map.Entry<Object, Object> entry :
entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
}
public void rollback() {
// 回滚时丢弃暂存数据
entriesToAddOnCommit.clear();
}
}
跨会话共享示例
ini
//会话1
SqlSession session1 = sqlSessionFactory.openSession();
UserMapper mapper1 = session1.getMapper(UserMapper.class);
User user1 = mapper1.selectById(1L);
System.out.println("会话1查询: " + user1);
session1.commit(); //提交,写入二级缓存
session1.close();
//会话2
SqlSession session2 = sqlSessionFactory.openSession();
UserMapper mapper2 = session2.getMapper(UserMapper.class);
User user2 = mapper2.selectById(1L); //从二级缓存获取
System.out.println("会话2查询: " + user2);
//对比结果
System.out.println("同一对象? " + (user1 == user2)); //false
System.out.println("值相等? " + user1.equals(user2)); //true
session2.close();
五、缓存命中流程
全景理解缓存的完整查询流程,是优化性能的关键。

完整查询链路
markdown
查询请求
↓
检查二级缓存
├─ 命中 → 直接返回
└─ 未命中
↓
检查一级缓存
├─ 命中 → 直接返回
└─ 未命中
↓
查询数据库
↓
写入一级缓存
↓
写入二级缓存(提交后)
↓
返回结果
源码实现
scss
public <E> List<E> query(MappedStatement ms, Object parameter, ...) {
BoundSql boundSql = ms.getBoundSql(parameter);
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
// 步骤1:查二级缓存
Cache cache = ms.getCache();
if (cache != null && ms.isUseCache()) {
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list != null) {
return list; // 二级缓存命中
}
}
// 步骤2:查一级缓存
List<E> list = (List<E>) localCache.getObject(key);
if (list != null) {
return list; // 一级缓存命中
}
//步骤3:查数据库
list = queryFromDatabase(ms, parameter, ...);
//步骤4:写入缓存
localCache.putObject(key, list);
if (cache != null) {
tcm.putObject(cache, key, list);
}
return list;
}
六、装饰器模式的运用
MyBatis缓存的装饰器设计堪称经典,让我们看看如何"给缓存穿衣服"。

LruCache:智能淘汰
typescript
public class LruCache implements Cache {
private final Cache delegate;
private Map<Object, Object> keyMap; // LinkedHashMap实现LRU
private Object eldestKey;
@Override
public Object getObject(Object key) {
keyMap.get(key); //访问即刷新顺序
return delegate.getObject(key);
}
@Override
public void putObject(Object key, Object value) {
delegate.putObject(key, value);
cycleKeyList(key); //淘汰最久未用的
}
}
ScheduledCache:定时清理
typescript
public class ScheduledCache implements Cache {
private long clearInterval = 3600000; // 1小时
private long lastClear;
@Override
public Object getObject(Object key) {
if (System.currentTimeMillis() - lastClear > clearInterval) {
clear(); //时间到,清空缓存
return null;
}
return delegate.getObject(key);
}
}
SerializedCache:深拷贝保护
typescript
public class SerializedCache implements Cache {
@Override
public void putObject(Object key, Object value) {
// 序列化存储
delegate.putObject(key, serialize((Serializable) value));
}
@Override
public Object getObject(Object key) {
// 反序列化返回,每次都是新对象
Object object = delegate.getObject(key);
return object == null ? null : deserialize((byte[]) object);
}
}
SynchronizedCache:线程安全卫士
typescript
public class SynchronizedCache implements Cache {
@Override
public synchronized void putObject(Object key, Object value) {
delegate.putObject(key, value);
}
@Override
public synchronized Object getObject(Object key) {
return delegate.getObject(key);
}
}
装饰器链的构建
scss
private Cache setStandardDecorators(Cache cache) {
//按顺序穿衣服
if (blocking) {
cache = new BlockingCache(cache);
}
if (readWrite) {
cache = new SerializedCache(cache);
}
if (scheduled) {
cache = new ScheduledCache(cache);
}
if (logging) {
cache = new LoggingCache(cache);
}
if (sync) {
cache = new SynchronizedCache(cache);
}
//LRU通常是最外层
cache = new LruCache(cache);
return cache;
}
七、最佳实践
推荐做法
ini
1.一级缓存- 保持默认开启,适合单会话重复查询
2.二级缓存- 仅在读多写少的场景开启
3.LRU策略- 大多数场景的最佳选择
4.合理设置容量- 根据业务量评估,避免内存溢出
5.只读缓存- 不可变对象使用
readOnly="true"
避免做法
1.在频繁更新的表上开启二级缓存
2.缓存大对象或包含敏感信息的对象
3.忽略缓存带来的数据一致性问题
4.不监控缓存命中率就盲目使用
性能优化技巧
- 热点数据优先
- 合理设置TTL
- 只读缓存加速
- 监控命中率
xml
<!-- 核心业务表单独配置 -->
<cache size="2048" eviction="LRU"/>
xml
<!-- 根据数据更新频率设置 -->
<cache flushInterval="300000"/> <!-- 5分钟 -->
xml
<!-- 不可变数据使用只读缓存 -->
<cache readOnly="true"/>
xml
<!-- 开启日志记录 -->
<cache>
<property name="logging" value="true"/>
</cache>
常见问题速查
问题1:二级缓存不生效
xml
<!-- 解决方案 -->
<!-- 1. 检查全局配置 -->
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
<!-- 2. 检查Mapper配置 -->
<cache/>
<!-- 3. 确保实体类实现Serializable -->
public class User implements Serializable {
private static final long serialVersionUID = 1L;
}
问题2:数据不一致
xml
<!-- 解决方案:及时刷新缓存 -->
<update id="updateUser" flushCache="true">
UPDATE t_user SET name = #{name} WHERE id = #{id}
</update>
<!-- 或设置自动刷新 -->
<cache flushInterval="60000"/>
问题3:内存溢出
xml
<!-- 解决方案1:限制容量 -->
<cache size="512"/>
<!-- 解决方案2:使用软引用 -->
<cache eviction="SOFT"/>
<!-- 解决方案3:定时清理 -->
<cache flushInterval="3600000"/>
实战案例
场景:电商系统商品查询优化
xml
<mapper namespace="com.shop.mapper.ProductMapper">
<!--
商品信息变化不频繁,适合二级缓存
使用LRU淘汰策略
设置1小时自动刷新
容量2048,覆盖热门商品
-->
<cache eviction="LRU"
flushInterval="3600000"
size="2048"
readOnly="false"/>
<select id="selectById" resultType="Product">
SELECT * FROM t_product WHERE id = #{id}
</select>
<!--更新操作强制刷新缓存 -->
<update id="updateProduct" flushCache="true">
UPDATE t_product SET price = #{price} WHERE id = #{id}
</update>
</mapper>
八、总结
一级缓存
✅ SqlSession级别
✅ 默认开启
✅ 增删改自动清空
✅ 适合单会话重复查询
二级缓存
✅ Mapper级别
✅ 需手动配置
✅ 跨SqlSession共享
✅ 适合读多写少场景
装饰器模式
✅ 灵活组合功能
✅ 支持多种淘汰策略
✅ 可扩展自定义实现
CacheKey机制
✅ 多元素组成
✅ 确保唯一性
✅ 精确命中判断
缓存是提升性能的利器,但也是一把双刃剑。理解MyBatis缓存的工作原理,才能在实战中游刃有余。