MyBatis 二级缓存深度解析
上一章中,我们介绍了Executor的三种实现,还有一种实现没有介绍,那就是CachingExecutor,用于实现二级缓存。本章我们介绍一下Mybatis的二级缓存是如何实现的。在详细解读之前,我们先简单看下,二级缓存要如何使用。
一、二级缓存的开启与使用
二级缓存默认关闭(全局开关默认开启,每个Mapper默认关闭),需通过全局配置与Mapper配置共同启用。其核心设计是 “按需启用”,确保缓存只作用于需要的业务场景。
1. 全局开关:开启二级缓存支持
MyBatis 通过cacheEnabled全局配置控制二级缓存总开关,默认值为true(即默认支持二级缓存,但需配合 Mapper 配置才会生效)。可在mybatis-config.xml中显式配置:
<configuration>
<settings>
<!-- 开启二级缓存支持(默认true,可省略) -->
<setting name="cacheEnabled" value="true"/>
</settings>
</configuration>
注意:若cacheEnabled设为false,将禁用所有二级缓存,即使Mapper中配置了<cache>也无效。
2. Mapper 级别配置:指定缓存策略
二级缓存的精细控制在 Mapper 层,通过<cache>标签(XML)或@CacheNamespace注解(接口)启用,同时可配置缓存的过期策略、容量等参数。
(1)XML Mapper 配置示例
<!-- UserMapper.xml -->
<mapper namespace="com.example.mapper.UserMapper">
<!-- 开启二级缓存,配置缓存策略 -->
<cache
eviction="LRU" <!-- 淘汰策略:LRU(最近最少使用) -->
flushInterval="60000" <!-- 自动刷新间隔(毫秒),60秒 -->
size="1024" <!-- 最大缓存条目 -->
readOnly="false"/> <!-- 是否只读:false支持读写,false时写入缓存是会做序列化,读取时做反序列化,也就是做个深拷贝,这样即使修改缓存也不会有副作用 -->
<!-- 查询语句:默认使用二级缓存 -->
<select id="selectUserById" resultType="User">
SELECT id, name, age FROM user WHERE id = #{id}
</select>
<!-- 更新语句:默认会刷新缓存(清空当前namespace的二级缓存) -->
<update id="updateUser">
UPDATE user SET name = #{name} WHERE id = #{id}
</update>
</mapper>
(2)注解配置示例
// UserMapper.java
@CacheNamespace(
eviction = LruCache.class, // 淘汰策略
flushInterval = 60000, // 自动刷新间隔
size = 1024, // 最大条目
readWrite = true // 读写模式
)
public interface UserMapper {
@Select("SELECT id, name, age FROM user WHERE id = #{id}")
User selectUserById(Long id);
@Update("UPDATE user SET name = #{name} WHERE id = #{id}")
int updateUser(User user);
}
3. 序列化
一般readOnly属性,都会设置成false,除非你能保证读取缓存后一定不会修改缓存而引起副作用。因此实体类实现Serializable接口:
public class User implements Serializable {
private Long id;
private String name;
private Integer age;
// getter/setter
}
大功告成,通过以上配置,就可以愉快的使用Mybatis的二级缓存了。
二、二级缓存的核心问题与解决方案
在分析源码之前,我们先讨论下,二级缓存有哪些挑战。Mybatis的二级缓存是namespace级别的,或者说是Mapper级别的,是跨SqlSession的。SqlSession本身是不支持多线程的,因此SqlSession级别的一级缓存是不需要考虑并发的。但在二级缓存场景下,必须考虑并发问题,这也导致二级缓存变得复杂起来。
1. 缓存并发问题
问题:二级缓存可能存在并发的读写,因此之前PerpetualCache直接使用HashMap实现的缓存,会有并发问题。
解决方案:Mybatis采用装饰者模式,使用SynchronizedCache为Cache做一层装饰,以实现Cache读取的同步。SynchronizedCache实现也比较简单,缓存的读写方法,都使用synchronized修饰,以达到同步读取的效果。
2. 事务可见性问题
问题:假设我们简单的使用一级缓存的方式实现二级缓存,那么在多线程的场景下,则可能:
sequenceDiagram
participant 线程1
participant 线程1的SqlSession
participant 二级缓存
participant 数据库
participant 线程2
participant 线程2的SqlSession
线程1->>线程1的SqlSession: 开启SqlSession(开启事务)
线程1->>线程1的SqlSession: 执行update(id=1) 【更新数据】
线程1的SqlSession->>数据库: 执行UPDATE语句
database-->>线程1的SqlSession: 更新成功(事务未提交)
线程1的SqlSession->>二级缓存: 清空当前namespace缓存(默认行为)
线程1->>线程1的SqlSession: 执行select(id=1) 【查询更新后的数据】
线程1的SqlSession->>数据库: 执行SELECT语句(因缓存已清空)
database-->>线程1的SqlSession: 返回id=1的新值(未提交的事务内可见)
Note over 线程1: 错误场景:直接将查询结果写入二级缓存
线程1的SqlSession->>二级缓存: 写入id=1的新值(未提交数据)
线程1的SqlSession-->>线程1: 返回查询结果
线程2->>线程2的SqlSession: 开启SqlSession
线程2->>线程2的SqlSession: 执行select(id=1) 【查询同一数据】
线程2的SqlSession->>二级缓存: 命中缓存
二级缓存-->>线程2的SqlSession: 返回id=1的新值(脏数据)
线程2的SqlSession-->>线程2: 读取到未提交的脏数据
Note over 线程1: 最终线程1回滚事务...
线程1->>线程1的SqlSession: 事务回滚
线程1的SqlSession->>数据库: 回滚update操作
database-->>线程1的SqlSession: 回滚成功
Note over 二级缓存,线程2: 问题:线程2已读取到缓存中的脏数据,且不会自动刷新
简单的说,如果线程A更新了id=1的数据之后,又查询了这条数据,就可以查出新修改的数据,如果直接放入二级缓存中,则存在问题:
- 直接把未提交的数据放到了二级缓存中,导致其他线程可见,破坏了数据库的隔离级别,导致可以读到未提交的数据。
- 事务可能回滚,造成二级缓存存在脏数据。
解决方案:还是使用装饰者模式,使用TransactionalCache在Cache上再套一层事务缓存。查询之后的数据并不会立即更新达到二级缓存,而是先暂存在TransactionalCache上,提交后再更新到二级缓存。回滚则直接丢弃。
3. 缓存一致性:如何保证缓存与数据库同步
问题:当数据被更新(如update/insert/delete)时,二级缓存中的旧数据需及时失效,否则会导致查询结果不一致。
解决方案:
- 执行更新操作时,清空缓存。
-
默认策略:MyBatis 默认对
update/insert/delete操作触发缓存刷新(清空当前Mapper命名空间的二级缓存)。 -
手动控制:也可通过
@Options(flushCache = FlushCachePolicy.FALSE)或<select flushCache="true">手动控制Mapper方法是否刷新缓存。
-
- 周期清理,可以设置
flushInterval参数,设置缓存清理的时间间隔。实现类是ScheduledCache,还是装饰器模式,每当get/put等方法被调用时,都会触发时间间隔的检查,假如超过了指定时间,清空缓存。
4. 缓存淘汰策略
问题:缓存不可能无限大,当缓存超过一定阈值时,需要淘汰策略。
解决方案:Mybatis提供了四种淘汰策略,LRU和FIFO可以配置size参数使用,当缓存数量超过阈值时,触发清理。SOFT、WEAK策略,则依赖JVM的垃圾回收。都是基于装饰者模式实现的。
| 策略 | 含义 | 适用场景 | 实现类 |
|---|---|---|---|
| LRU | 最近最少使用(Least Recently Used):移除最长时间未被使用的条目 | 大部分场景,优先保留热点数据 | LruCache |
| FIFO | 先进先出(First In First Out):按插入顺序移除最早的条目 | 数据访问顺序固定的场景 | FifoCache |
| SOFT | 软引用:基于 JVM 的软引用机制,内存不足时移除 | 允许缓存溢出的场景(依赖 JVM GC) | SoftCache |
| WEAK | 弱引用:基于 JVM 的弱引用机制,GC 时立即移除 | 缓存数据生命周期短的场景 | WeakCache |
4. 缓存对象本身的读写问题
问题:缓存可以跨SqlSession共享,也就是可以多线程共享,那么一个线程获取到缓存后,如果修改,则会直接修改缓存中的数据,进而影响到其他线程。
解决方案:深拷贝,实现类为SerializedCache,还是装饰者模式,写入缓存是进行序列化,读取时反序列化,相当于做了一次深拷贝。可以通过readOnly参数控制,如果为true,则不进行序列化,也就是用户自己注意,从缓存中读取的数据是只读的,不会被修改。
三、二级缓存的源码实现
从上文的解决方案中,可以发现,Mybatis非常钟爱装饰器模式(或者叫做责任链、代理模式,都一样),没有什么问题,是加一层装饰器不能解决的。
1. CachingExecutor & 装饰器模式
装饰器模式(Decorator Pattern)的核心是在不修改原有对象结构和代码的前提下,通过 “包装” 方式动态扩展对象功能。MyBatis的CachingExecutor是这一模式的经典实现 —— 它通过装饰基础 Executor(如 SimpleExecutor),为其添加二级缓存相关功能,同时保持 Executor 接口的一致性。以下结合源码详细解析。
装饰器模式的核心要素
装饰器模式有三个核心组成部分,在 CachingExecutor 中体现得淋漓尽致:
- 抽象组件(Component) :定义统一接口,是被装饰者和装饰者的共同父类 / 接口。
在 MyBatis 中对应Executor接口,定义了数据库操作的核心方法(query、update、commit等)。 - 具体组件(ConcreteComponent) :实现抽象组件的原始对象,是被装饰的目标。
在 MyBatis 中对应SimpleExecutor、ReuseExecutor、BatchExecutor等基础执行器,负责实际的 SQL 执行逻辑。 - 装饰器(Decorator) :实现抽象组件接口,持有具体组件的引用,通过包装具体组件扩展功能。
在 MyBatis 中 CachingExecutor 就是装饰器,它持有基础 Executor 的引用,同时也实现了 Executor 接口,这样,就可以在Executor接口基础上添加缓存相关逻辑:
// 抽象组件:Executor接口
public interface Executor {
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;
int update(MappedStatement ms, Object parameter) throws SQLException;
void commit(boolean required) throws SQLException;
void rollback(boolean required) throws SQLException;
// 其他核心方法...
}
// 装饰器:CachingExecutor实现Executor接口
public class CachingExecutor implements Executor {
// 持有被装饰的基础Executor(具体组件)
private final Executor delegate;
// 缓存管理器,用于管理二级缓存
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
// 构造函数:接收被装饰的Executor实例
public CachingExecutor(Executor delegate) {
this.delegate = delegate;
delegate.setExecutorWrapper(this); // 让基础Executor知道自己被装饰了
}
// 实现Executor接口的所有方法...
}
下面我们简单看下CachingExecutor是如何在原有的Executor的基础上,实现二级缓存的。
(1)query 方法:查询时先查缓存,未命中再查数据库
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 1. 获取SQL和参数,生成缓存key
BoundSql boundSql = ms.getBoundSql(parameter);
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
// 2. 调用带缓存key的重载方法
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
// 3. 获取当前Mapper的二级缓存
Cache cache = ms.getCache();
if (cache != null) {
// 4. 若需要,先刷新缓存(如配置了flushCache=true)
flushCacheIfRequired(ms);
// 5. 若启用缓存且无结果处理器,尝试从缓存获取
if (ms.isUseCache() && resultHandler == null) {
// 校验是否有输出参数(存储过程场景)
ensureNoOutParams(ms, boundSql);
// 6. 从二级缓存获取数据
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
// 7. 缓存未命中,调用基础Executor的query方法查数据库
if (list == null) {
list = delegate.query(ms, parameter, rowBounds, resultHandler, key, boundSql);
// 8. 将数据库查询结果存入二级缓存
tcm.putObject(cache, key, list);
}
return list;
}
}
// 9. 若未启用缓存,直接调用基础Executor查数据库
return delegate.query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
装饰逻辑:
在调用基础 Executor 的 query 方法(delegate.query(...))前,先检查二级缓存;若缓存未命中,查询数据库后再将结果存入缓存。这一过程完全不修改 SimpleExecutor 等基础执行器的代码,仅通过装饰器扩展了 “缓存查询” 功能。
(2)update 方法:更新时刷新缓存,保证数据一致性
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
// 1. 若需要,先刷新缓存(增删改默认flushCache=true)
flushCacheIfRequired(ms);
// 2. 调用基础Executor的update方法执行实际更新
return delegate.update(ms, parameter);
}
// 刷新缓存的具体逻辑:注意这里与查询是清缓存是一个方法,具体是否清理还要看用户配置,默认修改清缓存,查询不清除缓存,用户可以通过flushCache配置改变默认行为
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
// 若缓存存在且当前SQL需要刷新缓存(如增删改或配置了flushCache=true)
if (cache != null && ms.isFlushCacheRequired()) {
tcm.clear(cache); // 清空二级缓存
}
}
装饰逻辑:
在调用基础 Executor 的 update 方法前,先根据配置清空二级缓存,避免缓存中留存旧数据。这一 “刷新缓存” 的功能同样是通过装饰器添加,不影响原始 update 方法的执行逻辑。
(3)commit/rollback 方法:同步事务状态与缓存
@Override
public void commit(boolean required) throws SQLException {
// 1. 调用基础Executor的commit方法提交事务
delegate.commit(required);
// 2. 提交事务时,同步二级缓存(将暂存的缓存数据写入实际缓存)
tcm.commit();
}
@Override
public void rollback(boolean required) throws SQLException {
try {
// 1. 调用基础Executor的rollback方法回滚事务
delegate.rollback(required);
} finally {
if (required) {
// 2. 回滚事务时,清理二级缓存中暂存的无效数据
tcm.rollback();
}
}
}
装饰逻辑:
在事务提交 / 回滚时,通过 TransactionalCacheManager 同步二级缓存的状态(提交时写入缓存,回滚时丢弃无效数据),确保缓存与数据库事务的一致性。这一功能是对基础事务操作的扩展。
2. 事务缓存管理:TransactionalCache 与 TransactionalCacheManager
回顾下CachingExecutor的源码,先从MappedStatement(ms)中获取缓存,如果存在,使用TransactionalCacheManager(tcm)获取缓存。
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
// 3. 获取当前Mapper的二级缓存
Cache cache = ms.getCache();
if (cache != null) {
// 4. 若需要,先刷新缓存(如配置了flushCache=true)
flushCacheIfRequired(ms);
// 5. 若启用缓存且无结果处理器,尝试从缓存获取
if (ms.isUseCache() && resultHandler == null) {
// 校验是否有输出参数(存储过程场景)
ensureNoOutParams(ms, boundSql);
// 6. 从二级缓存获取数据
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
// 7. 缓存未命中,调用基础Executor的query方法查数据库
if (list == null) {
list = delegate.query(ms, parameter, rowBounds, resultHandler, key, boundSql);
// 8. 将数据库查询结果存入二级缓存
tcm.putObject(cache, key, list);
}
return list;
}
}
// 9. 若未启用缓存,直接调用基础Executor查数据库
return delegate.query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
TransactionalCacheManager
看到这你可能有点疑惑,已经通过MappedStatement(ms)中获取了二级缓存,为什么还要在通过TransactionalCacheManager去存取缓存呢?
还记得我们前文提到的事务可见性的问题么,Mybatis需要在事务提交后,才能更新缓存,因此需要暂存下未提交的数据,这是通过TransactionalCache装饰Cache实现的。二级缓存是Mapper级别的,而一个Executor(Executor的生命周期与SqlSession是一致的)可能会执行多个Mapper,它与二级缓存是一对多的关系,因此需要维护Cache和TransactionalCache的关系,这是通过也TransactionalCacheManager实现的:
public class TransactionalCacheManager {
//维护Cache与TransactionalCache的关系
private final Map<Cache, TransactionalCache> TransactionalCaches = new HashMap<>();
// 从缓存获取数据(实际从TransactionalCache获取)
public Object getObject(Cache cache, CacheKey key) {
return getTransactionalCache(cache).getObject(key);
}
// 向缓存存入数据(实际存入TransactionalCache的临时容器)
public void putObject(Cache cache, CacheKey key, Object value) {
getTransactionalCache(cache).putObject(key, value);
}
// 提交事务:将TransactionalCache中的临时数据刷入实际缓存
public void commit() {
for (TransactionalCache txCache : TransactionalCaches.values()) {
txCache.commit();
}
}
// 回滚事务:清空TransactionalCache中的临时数据
public void rollback() {
for (TransactionalCache txCache : TransactionalCaches.values()) {
txCache.rollback();
}
}
// 获取缓存对应的TransactionalCache(懒创建)
private TransactionalCache getTransactionalCache(Cache cache) {
return TransactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
}
}
TransactionalCache
TransactionalCache是事务缓存的核心实现。
- 内部属性
//被代理对象
private final Cache delegate;
//记录clear方法是否被调用,如果被调用,不会立即清空,而是提交时才会清空,因为即使调用了update方法,也是提交了才生效。
private boolean clearOnCommit;
//记录待更新的缓存
private final Map<Object, Object> entriesToAddOnCommit;
//记录未命中的缓存,与BlockingCache有关
private final Set<Object> entriesMissedInCache;
- 清空缓存 如果发生了更新,会调用clear方法,不会立即清空,而是提交时才会清空,因为即使调用了update方法,也是提交了才生效。
@Override
public void clear() {
//clear被调用,也就是发生了update操作,不会立即失效,提交时才会执行真正的clear
clearOnCommit = true;
entriesToAddOnCommit.clear();
}
- 缓存读取
@Override
public Object getObject(Object key) {
// 1. 从底层缓存读取数据
Object object = delegate.getObject(key);
// 2. 若未命中,记录key(用于BlockingCache释放锁)
if (object == null) {
entriesMissedInCache.add(key);
}
// 3. 若标记为"提交时清空缓存",也就是更新过,则直接返回 null(避免读取旧数据)
if (clearOnCommit) {
return null;
} else {
return object;
}
}
- 缓存写入(putObject)
事务期间写入缓存的操作不会直接同步到底层缓存,而是暂存到 entriesToAddOnCommit,延迟生效,确保未提交事务的中间结果不会被其他事务读取,符合事务隔离性。
@Override
public void putObject(Object key, Object value) {
// 暂存键值对,待事务提交时写入底层缓存
entriesToAddOnCommit.put(key, value);
}
- 事务提交
大致流程:
- 发生过更新(
clear被调用过),则清空缓存 - 提交暂存数据,释放查询为null的锁(与
BlockingCache有关,后面再讲) - 状态重置
public void commit() {
// 1. 若标记为"提交时清空",则先清空底层缓存
if (clearOnCommit) {
delegate.clear();
}
// 2. 将暂存的键值对写入底层缓存,并处理未命中的 key
flushPendingEntries();
// 3. 重置事务状态(清空暂存集合,重置标记)
reset();
}
// 同步暂存的缓存操作到二级缓存
private void flushPendingEntries() {
// a. 写入事务期间暂存的键值对
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
// b. 处理未命中的key,释放锁
for (Object key : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(key)) {
delegate.removeObject(key);
}
}
}
//状态重置
private void reset() {
clearOnCommit = false;
entriesToAddOnCommit.clear();
entriesMissedInCache.clear();
}
- 事务回滚(rollback)
事务回滚时,丢弃暂存的缓存操作,并释放查询为null的锁:
public void rollback() {
// 1. 移除未命中的 key(避免其他事务读到无效数据)
unlockMissedEntries();
// 2. 重置事务状态
reset();
}
// 释放锁
private void unlockMissedEntries() {
for (Object key : entriesMissedInCache) {
delegate.removeObject(key);
}
}
3. 缓存的构建
Mybatis的二级缓存是Mapper级别的,缓存对象是在解析Mapper配置时创建的,存储在Mybatis的Configuration中。
1、构造入口:Mapper 加载时的缓存初始化
MyBatis 启动时会解析所有 Mapper 接口(或 XML 文件),当检测到 Mapper 配置了二级缓存(如 <cache> 标签或 @CacheNamespace 注解)时,会触发 Cache 对象的构造,以 XML 解析为例:
// XMLMapperBuilder 解析 <cache> 标签的逻辑
private void cacheElement(XNode context) {
if (context != null) {
// 解析 cache 标签的属性(eviction、flushInterval、size、readOnly 等)
String type = context.getStringAttribute("type", "PERPETUAL");
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
String eviction = context.getStringAttribute("eviction", "LRU");
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
Long flushInterval = context.getLongAttribute("flushInterval");
Integer size = context.getIntAttribute("size");
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
boolean blocking = context.getBooleanAttribute("blocking", false);
Properties props = context.getChildrenAsProperties();
// 构建 Cache 对象,并与当前 Mapper 命名空间绑定
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}
builderAssistant 是 MapperBuilderAssistant 的实例,它负责协调 Mapper 相关对象的创建,包括 Cache 对象。
2、Cache 对象的实际构造:CacheBuilder
MapperBuilderAssistant.useNewCache() 方法会委托 CacheBuilder 完成 Cache 对象的构建,这是 Cache 实例化的核心过程:
// MapperBuilderAssistant.useNewCache()
public Cache useNewCache(Class<? extends Cache> typeClass, Class<? extends Cache> evictionClass, Long flushInterval, Integer size, boolean readWrite, boolean blocking, Properties props) {
// 1. 确定缓存实现类(默认 PerpetualCache)
typeClass = valueOrDefault(typeClass, PerpetualCache.class);
evictionClass = valueOrDefault(evictionClass, LruCache.class);
// 2. 创建 CacheBuilder 并配置参数
Cache cache = new CacheBuilder(currentNamespace) // 以 Mapper 命名空间为缓存 ID
.implementation(typeClass)
.addDecorator(evictionClass)
.flushInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.blocking(blocking)
.properties(props)
.build(); // 触发 Cache 对象的构建
// 3. 将构建好的 Cache 对象注册到 Configuration 中
configuration.addCache(cache);
// 4. 绑定当前 Mapper 命名空间与该 Cache 对象
currentCache = cache;
return cache;
}
经过上述流程后,就根据配置得到了一个完整的缓存装饰器链。CacheBuilder的构建过程:
// CacheBuilder.java(缓存构建器)
public Cache build() {
// 设置默认实现(PerpetualCache)
setDefaultImplementations();
// 创建基础缓存(PerpetualCache)
Cache cache = newBaseCacheInstance(implementation, id);
// 注入属性(如size、flushInterval)
setCacheProperties(cache);
// 若不是自定义缓存,添加默认装饰器链
if (PerpetualCache.class.equals(cache.getClass())) {
for (Class<? extends Cache> decorator : decorators) {
cache = newCacheDecoratorInstance(decorator, cache);
setCacheProperties(cache);
}
// 应用标准装饰器(如LruCache、ScheduledCache等)
cache = setStandardDecorators(cache);
} else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
// 添加日志装饰器
cache = new LoggingCache(cache);
}
return cache;
}
private Cache setStandardDecorators(Cache cache) {
try {
MetaObject metaCache = SystemMetaObject.forObject(cache);
if (size != null && metaCache.hasSetter("size")) {
metaCache.setValue("size", size);
}
// 如果设置了清空缓存的间隔时间,构造ScheduledCache装饰器
if (clearInterval != null) {
cache = new ScheduledCache(cache);
((ScheduledCache) cache).setClearInterval(clearInterval);
}
// 如果readOnly=false,构造SerializedCache装饰器
if (readWrite) {
cache = new SerializedCache(cache);
}
// 日志装饰器,打印debug日志,统计命中率
cache = new LoggingCache(cache);
// 同步装饰器
cache = new SynchronizedCache(cache);
// 阻塞装饰器
if (blocking) {
cache = new BlockingCache(cache);
}
return cache;
} catch (Exception e) {
throw new CacheException("Error building standard cache decorators. Cause: " + e, e);
}
}
缓存装饰器和作用:
| 装饰器类名 | 核心功能 | 关键字段 / 机制 | 适用场景 | 装饰器链位置(典型) |
|---|---|---|---|---|
| 基础功能装饰器 | ||||
PerpetualCache | 基础缓存实现,提供内存存储(非装饰器,是所有装饰器的底层实现) | Map<Object, Object> cache(基于 HashMap 存储) | 作为二级缓存的基础存储载体 | 最底层 |
SynchronizedCache | 为缓存操作添加同步锁,保证线程安全 | 所有方法添加 synchronized 关键字 | 多线程并发访问缓存场景 | 靠近底层(如 PerpetualCache 之上) |
| 缓存策略装饰器 | ||||
LruCache | 实现 LRU(最近最少使用)淘汰策略,限制缓存大小 | LinkedHashMap<Object, Object> keyMap(按访问顺序排序),size(最大容量) | 缓存空间有限,需自动淘汰不常用数据 | 基础缓存之上(如 PerpetualCache 外层) |
FifoCache | 实现 FIFO(先进先出)淘汰策略 | Deque<Object> keyList(记录插入顺序),size(最大容量) | 数据按时间顺序失效的场景 | 基础缓存之上 |
SoftCache | 使用软引用(SoftReference)存储缓存项,内存不足时自动回收 | Map<Object, SoftReference<Object>> cache | 允许缓存项在内存紧张时被回收,避免 OOM | 基础缓存之上 |
WeakCache | 使用弱引用(WeakReference)存储缓存项,GC 时自动回收 | Map<Object, WeakReference<Object>> cache | 缓存项生命周期与对象引用绑定,适合临时数据 | 基础缓存之上 |
| 功能扩展装饰器 | ||||
LoggingCache | 记录缓存命中率日志,方便调试和性能分析 | hitCount(命中次数)、requestCount(请求次数) | 需要监控缓存效果(命中率)的场景 | 中层装饰(如策略装饰器之上) |
SerializedCache | 对缓存值进行序列化 / 反序列化,实现对象深拷贝 | 重写 putObject/getObject,使用 ObjectOutputStream/ObjectInputStream | readOnly=false 时,保证缓存对象线程安全(避免修改共享实例) | 策略装饰器之上 |
BlockingCache | 缓存未命中时加锁,避免并发查询穿透到数据库 | ConcurrentHashMap<Object, CountDownLatch> locks(锁集合) | 高并发场景,防止缓存击穿(热点 key 失效时大量请求冲击数据库) | 靠近顶层(装饰器链外层) |
ScheduledCache | 按固定时间间隔自动清空缓存 | long flushInterval(刷新间隔)、long lastFlushTime(上次刷新时间) | 数据定时更新,需定期失效的场景(如每小时刷新一次) | 基础缓存之上 |
3. Cache 对象的存储:Configuration -> MappedStatement
构建完成的 Cache 对象会被存储在 Configuration 类的 caches 集合中,这是 MyBatis 全局管理二级缓存的容器:
// Configuration 类中存储缓存的字段
protected final Map<String, Cache> caches = new StrictMap<>("Caches collection");
// 添加缓存到注册表
public void addCache(Cache cache) {
caches.put(cache.getId(), cache); // 以缓存 ID(即 Mapper 命名空间)为 key
}
// 获取缓存(后续执行 SQL 时会调用此方法)
public Cache getCache(String id) {
return caches.get(id);
}
每个 Cache 对象的 id 就是对应的 Mapper 命名空间(如 com.example.mapper.UserMapper),这确保了二级缓存以 Mapper 为单位隔离。后续配置解析时,会进一步构建Mapper每个方法的MappedStatement,它封装了 Mapper 接口中单个方法(或XML中单个SQL标签)的所有配置信息。如果开启了缓存,MappedStatement会引用对应Cahce。
总结
MyBatis二级缓存通过 “装饰器模式” 构建了灵活可扩展的缓存体系,设计很精巧,然而实践中使用的并不多:
- 分布式环境下的缓存共享难题,默认实现是单机模式,而现在多数是分布式环境,不同服务节点的缓存无法共享,缓存过期,不同的节点间也不会相互通知。当然,我们可以通过自定义缓存,实现
Cache接口,借助Redis等来实现缓存,再配置中设置自己的实现类即可:<cache type="com.example.cache.RedisCache"/>。但依然难以解决缓存一致性问题。 - 缓存失效的方式过于粗暴,只要发生了修改,不管具体更新了多少数据,所有的缓存都会生效。没有自行管理缓存灵活。