0%

mybatis之缓存

MyBatis 缓存机制深度解析:从 Cache 接口到装饰器模式

MyBatis 缓存是提升查询性能的核心组件,通过减少重复数据库访问,大幅降低系统开销。其缓存体系基于 Cache 接口构建,采用 装饰器模式 实现 “基础存储 + 功能增强” 的灵活扩展,同时通过 CacheKey 保证缓存键的唯一性。从 “缓存体系架构→Cache 接口与实现→CacheKey 生成逻辑→实战配置” 四个维度,彻底拆解 MyBatis 缓存的底层机制。

MyBatis 缓存体系总览

MyBatis 提供 两级缓存,本质上均基于 Cache 接口实现,核心区别在于 “作用范围” 和 “生命周期”:

缓存级别 作用范围 生命周期 默认状态 底层核心实现 典型场景
一级缓存 SqlSession 内部(会话级) 随 SqlSession 关闭而销毁 开启 PerpetualCache(HashMap) 同一会话内的重复查询(如单事务内多次查同一数据)
二级缓存 Mapper namespace(接口级) 随 MyBatis 应用生命周期 关闭 PerpetualCache + 装饰器(如 LRU 淘汰、序列化) 跨会话的重复查询(如多用户查询同一商品信息)

核心设计思想:装饰器模式

MyBatis 缓存的灵活性源于 装饰器模式

  • 基础组件PerpetualCache 实现最基本的缓存存储(基于 HashMap),是所有缓存的 “底层容器”;
  • 装饰器组件:如 LruCache(LRU 淘汰)、BlockingCache(并发控制)、SerializedCache(序列化)等,通过包装 PerpetualCache 或其他装饰器,动态增强缓存功能;
  • 组合能力:可按需组合多个装饰器(如 “LRU 淘汰 + 序列化 + 日志”),满足复杂业务需求。

Cache 接口:缓存的标准定义

Cache 接口是 MyBatis 缓存的顶层规范,定义了缓存的 7 个核心行为,所有缓存实现类均需遵守该接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public interface Cache {
// 1. 获取缓存唯一标识(通常为 Mapper namespace,如 com.example.UserMapper)
String getId();

// 2. 存入缓存(key 为 CacheKey,value 为查询结果)
void putObject(Object key, Object value);

// 3. 获取缓存(通过 key 查找)
Object getObject(Object key);

// 4. 删除缓存(通过 key 移除,默认返回 null,部分实现返回被删除值)
Object removeObject(Object key);

// 5. 清空缓存
void clear();

// 6. 获取缓存大小(当前存储的键值对数量)
int getSize();

// 7. 获取读写锁(3.2.6 后废弃,并发控制由装饰器实现,如 SynchronizedCache)
ReadWriteLock getReadWriteLock();
}
接口设计要点:
  • 缓存键(key):统一使用 CacheKey 类型(而非简单 String/Integer),确保不同查询的唯一性;
  • 无状态设计:接口方法仅依赖入参,不保存额外状态,便于装饰器组合;
  • 功能最小化:仅定义基础缓存操作,复杂功能(如淘汰、同步)通过装饰器扩展。

Cache 接口实现类解析

MyBatis 提供 10+ Cache 实现类,可分为 基础存储类功能装饰器类,以下按功能分类解析核心实现。

1. 基础存储类:PerpetualCache(HashMap 实现)

PerpetualCache 是 MyBatis 缓存的 基础实现,仅提供 “键值对存储” 功能,无淘汰、同步等增强,是所有缓存的底层容器。

源码核心逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class PerpetualCache implements Cache {
private final String id; // 缓存唯一标识(namespace)
private final Map<Object, Object> cache = new HashMap<>(); // 底层存储容器

// 构造函数:必须传入 id(MyBatis 初始化时传入 namespace)
public PerpetualCache(String id) {
this.id = id;
}

// 存入缓存:直接调用 HashMap.put()
@Override
public void putObject(Object key, Object value) {
cache.put(key, value);
}

// 获取缓存:直接调用 HashMap.get()
@Override
public Object getObject(Object key) {
return cache.get(key);
}

// 删除缓存:直接调用 HashMap.remove()
@Override
public Object removeObject(Object key) {
return cache.remove(key);
}

// 清空缓存:调用 HashMap.clear()
@Override
public void clear() {
cache.clear();
}

// 获取缓存大小:返回 HashMap.size()
@Override
public int getSize() {
return cache.size();
}

// 其他方法(getId、equals、hashCode)省略...
}
特点与适用场景:
  • 优点:简单高效,基于 HashMap 实现 O (1) 读写复杂度;
  • 缺点:无缓存淘汰机制(满了不会自动删除)、无并发控制(多线程读写可能不安全);
  • 适用场景:作为一级缓存的底层存储(SqlSession 内单线程访问,无需淘汰),或作为二级缓存的基础容器(需配合装饰器使用)。

2. 缓存淘汰策略装饰器:解决 “缓存满了怎么办”

当缓存达到容量上限时,需通过淘汰策略删除部分数据,MyBatis 提供 3 种常用淘汰装饰器:

(1)FifoCache:先进先出(FIFO)

按 “缓存存入顺序” 淘汰数据,最早存入的缓存项优先被删除,适用于 “数据时效性按顺序变化” 的场景(如日志查询)。

源码核心逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class FifoCache implements Cache {
private final Cache delegate; // 被装饰的底层缓存(如 PerpetualCache)
private final Deque<Object> keyList; // 记录缓存项存入顺序的队列
private int size; // 缓存容量上限(默认 1024)

public FifoCache(Cache delegate) {
this.delegate = delegate;
this.keyList = new LinkedList<>();
this.size = 1024; // 默认容量 1024
}

// 存入缓存:先检查容量,满了则删除最早的缓存项
@Override
public void putObject(Object key, Object value) {
cycleKeyList(key); // 检查并淘汰数据
delegate.putObject(key, value); // 存入底层缓存
}

// 核心:检查容量,满了则删除队列头部(最早存入)的缓存项
private void cycleKeyList(Object key) {
keyList.addLast(key); // 新缓存项加入队列尾部
if (keyList.size() > size) { // 超过容量上限
Object oldestKey = keyList.removeFirst(); // 删除队列头部(最早的)
delegate.removeObject(oldestKey); // 从底层缓存删除
}
}

// 其他方法(getObject、clear 等)直接委托给 delegate...
}
关键逻辑:
  • LinkedList 记录缓存项顺序,新项入队尾,满了删队头;
  • 仅在 putObject 时触发淘汰,getObject 不改变顺序。
(2)LruCache:最近最少使用(LRU)

按 “缓存访问频率” 淘汰数据,最近最少被访问的缓存项优先被删除,适用于 “热点数据集中” 的场景(如商品详情查询),是最常用的淘汰策略。

源码核心逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class LruCache implements Cache {
private final Cache delegate;
private Map<Object, Object> keyMap; // 记录访问顺序的 Map
private Object eldestKey; // 即将被淘汰的 key(最少使用)

public LruCache(Cache delegate) {
this.delegate = delegate;
this.setSize(1024); // 默认容量 1024
}

// 初始化 keyMap:LinkedHashMap 按“访问顺序”排序(accessOrder=true)
public void setSize(final int size) {
keyMap = new LinkedHashMap<Object, Object>(size, 0.75F, true) {
@Override
protected boolean removeEldestEntry(Entry<Object, Object> eldest) {
// 当 Map 大小超过容量时,标记 eldestKey 为待淘汰 key
boolean tooBig = this.size() > size;
if (tooBig) {
LruCache.this.eldestKey = eldest.getKey();
}
return tooBig;
}
};
}

// 存入缓存:先检查淘汰,再存入底层缓存
@Override
public void putObject(Object key, Object value) {
delegate.putObject(key, value);
cycleKeyList(key); // 检查并淘汰最少使用的 key
}

// 获取缓存:触发 keyMap 的访问顺序更新(LinkedHashMap 会将当前 key 移到队尾)
@Override
public Object getObject(Object key) {
keyMap.get(key); // 仅触发访问顺序更新,不实际存储值
return delegate.getObject(key);
}

// 核心:删除 eldestKey 对应的缓存项
private void cycleKeyList(Object key) {
keyMap.put(key, key); // 将 key 加入 keyMap,触发 removeEldestEntry 检查
if (eldestKey != null) {
delegate.removeObject(eldestKey); // 淘汰最少使用的 key
eldestKey = null;
}
}

// 其他方法(clear 等)直接委托给 delegate...
}
关键逻辑:
  • 使用 LinkedHashMap 并设置 accessOrder=true,使 Map 按 “访问顺序” 排序(get/put 会将 key 移到队尾);
  • removeEldestEntry 方法在 Map 满时返回 true,标记 eldestKey 为待淘汰项,随后在 cycleKeyList 中删除。
(3)SoftCache/WeakCache:基于引用的淘汰(GC 触发)

通过 Java 的 软引用(SoftReference)弱引用(WeakReference) 实现缓存淘汰,当 JVM 内存不足时,自动回收缓存项,适用于 “非核心数据缓存”(如临时统计数据)。

核心区别:
实现类 引用类型 GC 回收时机 适用场景
SoftCache 软引用 JVM 内存不足时回收 允许缓存项临时存活,内存不足再淘汰
WeakCache 弱引用 下次 GC 时立即回收 缓存项生命周期短,随 GC 快速清理
SoftCache 关键源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class SoftCache implements Cache {
private final Cache delegate;
// 强引用队列:保存最近使用的缓存项(避免被 GC 回收)
private final Deque<Object> hardLinksToAvoidGarbageCollection;
// 引用队列:记录已被 GC 回收的缓存项
private final ReferenceQueue<Object> queueOfGarbageCollectedEntries;
private int numberOfHardLinks = 256; // 强引用保留数量(默认 256)

@Override
public void putObject(Object key, Object value) {
removeGarbageCollectedItems(); // 清理已被 GC 回收的缓存项
// 存入软引用包装的值(value 为软引用,key 为强引用)
delegate.putObject(key, new SoftEntry(key, value, queueOfGarbageCollectedEntries));
}

@Override
public Object getObject(Object key) {
Object result = null;
SoftReference<Object> softReference = (SoftReference) delegate.getObject(key);
if (softReference != null) {
result = softReference.get();
if (result == null) {
delegate.removeObject(key); // 软引用已被 GC,删除缓存项
} else {
// 将结果加入强引用队列,避免被 GC 回收
synchronized (hardLinksToAvoidGarbageCollection) {
hardLinksToAvoidGarbageCollection.addFirst(result);
if (hardLinksToAvoidGarbageCollection.size() > numberOfHardLinks) {
hardLinksToAvoidGarbageCollection.removeLast(); // 超出数量,删除最早的强引用
}
}
}
}
return result;
}

// 清理已被 GC 回收的缓存项(从引用队列中获取并删除)
private void removeGarbageCollectedItems() {
SoftEntry sv;
while ((sv = (SoftEntry) queueOfGarbageCollectedEntries.poll()) != null) {
delegate.removeObject(sv.key);
}
}

// 软引用包装类:关联 key 和引用队列
private static class SoftEntry extends SoftReference<Object> {
private final Object key;
SoftEntry(Object key, Object value, ReferenceQueue<Object> queue) {
super(value, queue);
this.key = key;
}
}
}

3. 并发控制装饰器:解决 “多线程安全问题”

当多个线程同时访问缓存时,可能出现 “并发修改异常” 或 “重复查询数据库”,MyBatis 提供 2 种并发控制装饰器:

(1)SynchronizedCache:同步锁(简单并发控制)

为缓存的所有方法添加 synchronized 关键字,保证单线程访问,适用于 “低并发” 场景。

源码核心逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class SynchronizedCache implements Cache {
private final Cache delegate;

public SynchronizedCache(Cache delegate) {
this.delegate = delegate;
}

// 所有方法均加 synchronized,保证单线程执行
@Override
public synchronized int getSize() {
return delegate.getSize();
}

@Override
public synchronized void putObject(Object key, Object value) {
delegate.putObject(key, value);
}

@Override
public synchronized Object getObject(Object key) {
return delegate.getObject(key);
}

// 其他方法(removeObject、clear)均加 synchronized...
}
特点:
  • 优点:实现简单,保证线程安全;
  • 缺点:全局锁,高并发下性能差(所有缓存操作串行执行)。
(2)BlockingCache:阻塞锁(避免重复查询)

每个缓存 key 单独加锁,保证 “同一 key 仅一个线程查询数据库”,其他线程阻塞等待,适用于 “高并发下同一 key 频繁查询” 的场景(如热点商品详情)。

源码核心逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public class BlockingCache implements Cache {
private long timeout; // 锁超时时间(默认无超时)
private final Cache delegate;
// 每个 key 对应一个 ReentrantLock(细粒度锁)
private final ConcurrentHashMap<Object, ReentrantLock> locks = new ConcurrentHashMap<>();

@Override
public Object getObject(Object key) {
acquireLock(key); // 获取当前 key 的锁(其他线程需等待)
Object value = delegate.getObject(key); // 查询缓存
if (value != null) {
releaseLock(key); // 缓存命中,释放锁
}
// 缓存未命中:不释放锁,直到 putObject 时释放(避免其他线程重复查库)
return value;
}

@Override
public void putObject(Object key, Object value) {
try {
delegate.putObject(key, value); // 存入缓存
} finally {
releaseLock(key); // 无论存入是否成功,均释放锁
}
}

// 获取 key 对应的锁(支持超时)
private void acquireLock(Object key) {
ReentrantLock lock = getLockForKey(key);
if (timeout > 0) {
try {
// 尝试获取锁,超时则抛异常
boolean acquired = lock.tryLock(timeout, TimeUnit.MILLISECONDS);
if (!acquired) {
throw new CacheException("获取锁超时:key=" + key);
}
} catch (InterruptedException e) {
throw new CacheException("获取锁被中断:key=" + key);
}
} else {
lock.lock(); // 无超时,阻塞等待
}
}

// 释放 key 对应的锁
private void releaseLock(Object key) {
ReentrantLock lock = locks.get(key);
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}

// 获取或创建 key 对应的锁
private ReentrantLock getLockForKey(Object key) {
return locks.computeIfAbsent(key, k -> new ReentrantLock());
}
}
关键逻辑:
  • 细粒度锁:每个 key 对应独立的 ReentrantLock,不同 key 可并行查询;
  • 锁释放时机:缓存命中时在 getObject 释放,未命中时在 putObject 释放(确保只有一个线程查库);
  • 超时保护:支持 timeout 配置,避免死锁导致线程永久阻塞。

4. 其他功能装饰器

装饰器类 核心功能 适用场景
LoggingCache 记录缓存命中次数和命中率(日志) 缓存性能监控(如 debug 时查看命中率)
ScheduledCache 周期性清理缓存(如每小时清空一次) 缓存数据有固定过期时间(如小时级统计数据)
SerializedCache 对缓存值进行序列化 / 反序列化 分布式缓存(如多节点共享缓存需序列化)

CacheKey:缓存键的生成逻辑(缓存命中的关键)

MyBatis 缓存的 key 并非简单的 “参数值”,而是通过 CacheKey 类生成的复合键,确保 “不同查询不会共用同一个 key”,避免缓存命中错误。

1. CacheKey 的组成部分

一个 CacheKey 由 5 个核心要素共同决定,任意一个要素不同,生成的 key 就不同

  1. MappedStatement ID:SQL 对应的唯一标识(如 com.example.UserMapper.selectUserById);
  2. RowBounds:查询结果范围(offset 偏移量和 limit 最大行数);
  3. SQL 语句:解析后的原始 SQL(含 ? 占位符,如 SELECT * FROM user WHERE id = ?);
  4. 查询参数:用户传入的实际参数(如 id=1);
  5. Environment ID:数据库环境标识(多数据源场景下,不同数据源的同一查询需不同 key)。

2. CacheKey 核心源码解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public class CacheKey implements Cloneable, Serializable {
private static final int DEFAULT_MULTIPLIER = 37; // 哈希计算乘数
private static final int DEFAULT_HASHCODE = 17; // 初始哈希值
private final int multiplier; // 乘数(默认 37)
private int hashcode; // 最终哈希值
private long checksum; // 校验和(避免哈希碰撞)
private int count; // updateList 元素个数
private List<Object> updateList; // 存储所有组成要素的列表

public CacheKey() {
this.hashcode = DEFAULT_HASHCODE;
this.multiplier = DEFAULT_MULTIPLIER;
this.count = 0;
this.updateList = new ArrayList<>();
}

// 核心:添加一个组成要素(如 MappedStatement ID、参数等)
public void update(Object object) {
// 1. 计算当前要素的哈希值(null 时为 1)
int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
count++; // 要素个数+1
checksum += baseHashCode; // 累加校验和
baseHashCode *= count; // 哈希值乘以要素个数(减少碰撞)
// 2. 更新最终哈希值(公式:hashcode = multiplier * hashcode + baseHashCode)
hashcode = multiplier * hashcode + baseHashCode;
updateList.add(object); // 加入要素列表
}

// 批量添加要素(如同时添加 SQL 和参数)
public void updateAll(Object[] objects) {
for (Object o : objects) {
update(o);
}
}

// 判断两个 CacheKey 是否相等(需所有要素完全一致)
@Override
public boolean equals(Object object) {
if (this == object) return true;
if (!(object instanceof CacheKey)) return false;

CacheKey cacheKey = (CacheKey) object;
// 1. 先比较哈希值、校验和、要素个数(快速排除不相等的情况)
if (hashcode != cacheKey.hashcode) return false;
if (checksum != cacheKey.checksum) return false;
if (count != cacheKey.count) return false;
// 2. 逐个比较 updateList 中的要素(确保完全一致)
for (int i = 0; i < updateList.size(); i++) {
Object thisObj = updateList.get(i);
Object thatObj = cacheKey.updateList.get(i);
if (!ArrayUtil.equals(thisObj, thatObj)) return false;
}
return true;
}

// 哈希值(基于 hashcode 字段)
@Override
public int hashCode() {
return hashcode;
}
}
关键逻辑:
  • 哈希计算:通过 update 方法累加每个要素的哈希值,使用 “37*hashcode + 要素哈希” 的公式,减少哈希碰撞;
  • equality 判断 :先通过 hashcodechecksumcount 快速排除不相等的 key,再逐个比较要素列表,确保完全一致;
  • 防碰撞checksum 字段进一步降低哈希碰撞概率,避免不同要素组合生成相同 hashcode。

3. CacheKey 生成时机

Executor 执行查询时,会调用 createCacheKey 方法生成 CacheKey

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// BaseExecutor 类
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql) {
if (closed) throw new ExecutorException("Executor 已关闭");
CacheKey cacheKey = new CacheKey();
// 1. 添加 MappedStatement ID
cacheKey.update(ms.getId());
// 2. 添加 RowBounds(offset 和 limit)
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
// 3. 添加 SQL 语句
cacheKey.update(boundSql.getSql());
// 4. 添加查询参数(处理参数值,避免引用类型影响)
cacheKey.updateAll(getParameterObjects(parameter, boundSql));
// 5. 添加 Environment ID(多数据源场景)
if (configuration.getEnvironment() != null) {
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}

实战:二级缓存配置与装饰器组合

一级缓存默认开启(无需配置),二级缓存需手动开启,且可通过配置组合多个装饰器,实现 “LRU 淘汰 + 周期性清理 + 序列化” 等功能。

1. 全局开启二级缓存(mybatis-config.xml)

1
2
3
4
5
6
<configuration>
<settings>
<!-- 全局开启二级缓存(默认 false) -->
<setting name="cacheEnabled" value="true"/>
</settings>
</configuration>

2. Mapper 接口开启二级缓存(Mapper.xml)

通过 <cache> 标签配置二级缓存,支持指定装饰器组合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<mapper namespace="com.example.mapper.UserMapper">
<!--
配置二级缓存:基础存储为 PerpetualCache,组合多个装饰器
eviction:淘汰策略(LRU/FIFO/SOFT/WEAK,默认 LRU)
flushInterval:清理间隔(毫秒,默认无,即不自动清理)
size:缓存容量(默认 1024)
readOnly:是否只读(true 时返回缓存对象的引用,false 时返回拷贝,默认 false)
type:自定义 Cache 实现类(默认 PerpetualCache)
-->
<cache
eviction="LRU"
flushInterval="60000"
size="1024"
readOnly="false"
type="org.apache.ibatis.cache.impl.PerpetualCache">
<!-- 为装饰器设置参数(如 BlockingCache 的超时时间) -->
<property name="timeout" value="5000"/>
</cache>

<!-- 其他 SQL 语句(如 select、insert 等) -->
<select id="selectUserById" resultType="User" useCache="true">
SELECT * FROM user WHERE id = #{id}
</select>
</mapper>
关键配置说明:
  • eviction="LRU":使用 LruCache 装饰器,实现 LRU 淘汰;
  • flushInterval="60000":使用 ScheduledCache 装饰器,每 60 秒清理一次缓存;
  • readOnly="false":使用 SerializedCache 装饰器,返回缓存对象的拷贝(避免并发修改);
  • useCache="true":指定该查询使用二级缓存(默认 true,insert/update/delete 会自动清空缓存)。

总结:MyBatis 缓存的核心价值与适用场景

MyBatis 缓存通过 Cache 接口和装饰器模式,实现了 “灵活扩展 + 性能优化” 的双重目标,核心价值体现在:

  1. 性能提升:减少重复数据库访问,尤其对热点数据查询(如商品详情),性能提升显著;
  2. 灵活扩展:装饰器模式支持按需组合功能(淘汰策略、并发控制、序列化等),满足不同业务需求;
  3. 低侵入性:无需修改业务代码,通过配置即可开启和定制缓存;
  4. 安全性CacheKey 确保缓存键的唯一性,避免命中错误数据;BlockingCache 等装饰器保证并发安全。

适用场景与注意事项:

  • 适用场景:查询频繁、修改少、数据一致性要求不高的场景(如商品分类、静态配置);
  • 不适用场景:实时性要求高的数据(如用户余额)、频繁修改的数据(如订单状态);
  • 注意事项:
    1. 二级缓存是 namespace 级别,跨 namespace 的关联查询可能导致数据不一致;
    2. 缓存对象需实现 Serializable 接口(如使用 SerializedCache 或分布式缓存);
    3. insert/update/delete 操作会自动清空对应 namespace 的二级缓存,确保数据一致性

欢迎关注我的其它发布渠道

表情 | 预览
快来做第一个评论的人吧~
Powered By Valine
v1.3.10