上一篇《Mybatis的Executor和缓存体系》介绍了一二级缓存的特点、启用和区别。二级缓存是Mapper级别,在整个应用中可以跨线程共享,所以有更高的命中率。和一级缓存的PerpetualCache相比,二级缓存机制和体系更加完整。
现在,我们一起来看看mybatis二级缓存的创建和实现。
注:本文中源码来自mybatis 3.4.x版本,地址https://github.com/mybatis/mybatis-3.git
一 Cache接口体系
根接口Cache中定义了缓存的操作方法,有如下约束:
- 子类实现必须有一个构造函数,该构造函数接收一个字符串类型的cache id作为参数;
- MyBatis 会将Mapper文件的namespace作为cache id,来创建Cache实例;
- 每个namespace都有一个cache实例。
在结构设计上,二级缓存采用装饰器模式,提供了很多扩展功能,如:
- 内存溢出回收机制,如FIFO、LRU、soft和weak引用;
- 序列化存储,如SerializedCache
- 缓存命中统计,LoggingCache

1.1 基础缓存PerpetualCache
- PerpetualCache是基础实现,是所有装饰器的被装饰对象;
- HashMap存储,简单的键值对存储;
- 数据永久保存,直到手动清理;
- 非线程安全。

1.2 淘汰策略装饰器
最近最少使用LruCache
- LRU算法:基于LinkedHashMap的访问顺序
- 容量控制:超出容量时淘汰最久未访问的数据
- 访问更新:每次get操作都会更新访问顺序
- 默认大小:1024个对象
java
public class LruCache implements Cache {
// 被装饰的Cache对象
private final Cache delegate;
// 最近最少使用算法的实现,基于LinkedHashMap
private Map<Object, Object> keyMap;
// 最近最少使用的key,用于移除过期的缓存项
private Object eldestKey;
public LruCache(Cache delegate) {
this.delegate = delegate;
setSize(1024);
}
public void setSize(final int size) {
keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
private static final long serialVersionUID = 4267176411845948333L;
@Override
protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
boolean tooBig = size() > size;
if (tooBig) {
eldestKey = eldest.getKey();
}
return tooBig;
}
};
}
// put时触发移除eldestKey
@Override
public void putObject(Object key, Object value) {
delegate.putObject(key, value);
cycleKeyList(key);
}
// get时触发更新eldestKey
@Override
public Object getObject(Object key) {
keyMap.get(key); //touch
return delegate.getObject(key);
}
先进先出FifoCache
- FIFO算法:严格按照插入顺序淘汰
- 队列实现:使用LinkedList维护插入顺序,默认大小为1024
- 简单高效:不考虑访问频率,只看插入时间
- 适用场景:数据访问模式相对均匀
java
public class FifoCache implements Cache {
private final Cache delegate;
private final Deque<Object> keyList; // LinkedList维护插入顺序
private void cycleKeyList(Object key) {
keyList.addLast(key); // 新key加到队尾
if (keyList.size() > size) {
Object oldestKey = keyList.removeFirst(); // 移除队首
delegate.removeObject(oldestKey);
}
}
}
1.3 引用类型装饰器
软引用缓存SoftCache
内存不足时(JVM在OOM前)自动清理,使用SoftReference包装缓存项
java
public class SoftCache implements Cache {
// 维护一定数量强引用,避免缓存项都被GC回收
private final Deque<Object> hardLinksToAvoidGarbageCollection;
// 监听GC队列,及时清理失效缓存
private final ReferenceQueue<Object> queueOfGarbageCollectedEntries;
private final Cache delegate;
@Override
public void putObject(Object key, Object value) {
// 先清理queueOfGarbageCollectedEntries中GC的缓存项
removeGarbageCollectedItems();
// 缓存项包装为SoftReference
delegate.putObject(key, new SoftEntry(key, value, queueOfGarbageCollectedEntries));
}
弱引用缓存WeakCache
类似SoftCache,但使用WeakReference,在下次GC时缓存项就可能被回收。
1.4 功能增强装饰器
序列化缓存SerializedCache
- 深拷贝:每次get返回新的对象实例
- 线程安全:避免多线程修改同一对象
- 序列化要求:对象必须实现Serializable接口
- 性能开销:序列化/反序列化有性能成本
java
public class SerializedCache implements Cache {
@Override
public void putObject(Object key, Object object) {
if (object == null || object instanceof Serializable) {
delegate.putObject(key, serialize((Serializable) object)); // 序列化存储
} else {
throw new CacheException("SharedCache failed to make a copy of a non-serializable object: " + object);
}
}
@Override
public Object getObject(Object key) {
Object object = delegate.getObject(key);
return object == null ? null : deserialize((byte[]) object); // 反序列化返回
}
}
同步缓存SynchronizedCache
由于PerpetualCache非线程安全,SynchronizedCache 中对所有方法都加synchronized,虽然变得线程安全了,但是高并发时会影响性能。
java
@Override
public synchronized void putObject(Object key, Object object) {
delegate.putObject(key, object);
}
日志缓存LoggingCache
能够统计和输出缓存命中率。

阻塞缓存BlockingCache
适用于高并发环境下防止缓存击穿、数据库查询成本很高的场景。
- 防止缓存击穿:多线程同时查询同一个不存在的key时,只有一个线程查询数据库
- 锁机制:每个key对应一个ReentrantLock
- 超时控制:支持获取锁的超时时间设置
java
// 阻塞缓存:当缓存未命中时, 会阻塞当前线程, 直到缓存被填充
public class BlockingCache implements Cache {
// 锁超时时间
private long timeout = 1000;
private final Cache delegate;
// 每个缓存键对应一个锁对象
private final ConcurrentHashMap<Object, ReentrantLock> locks;
// 放入缓存项,同时释放锁对象
@Override
public void putObject(Object key, Object value) {
try {
delegate.putObject(key, value);
} finally {
releaseLock(key);
}
}
@Override
public Object getObject(Object key) {
// 先获取缓存键对应的锁对象
acquireLock(key);
Object value = delegate.getObject(key);
// 如果缓存命中, 则释放锁对象
if (value != null) {
releaseLock(key);
}
// 未命中时返回null,将导致查询数据库、写缓存
return value;
}
定时清理缓存ScheduledCache
在缓存操作时检查是否超时,超时后自动清空所有缓存数据。支持设置时间间隔。适用于数据有明确的时效性要求、定期刷新缓存数据等场景。


1.5 总结
MyBatis的Cache体系通过装饰器模式,实现了高度的灵活性和可扩展性,每个装饰器专注于单一职责,可以根据实际需求灵活组合使用。
二 创建和使用二级缓存
2.1 创建
一级缓存是org.apache.ibatis.executor.BaseExecutor中PerpetualCache localCache,默认启用,在创建Executor时就初始化了。

那么,二级缓存是何时创建的呢?
先说结论:
- 二级缓存实例在应用启动的Mapper XML解析阶段创建,而不是在运行时动态创建。这保证了缓存的生命周期与应用生命周期一致,实现了跨SqlSession的数据共享。
- 每个Mapper.xml中<cache>标签,对应一个缓存实例,cacheId就是namespace,创建时就应用了所有装饰器。
详细流程:
- 在XMLConfigBuilder.mapperElement方法中,会逐个解析Mapper.xml;
java
// XMLConfigBuilder.parse() -> parseConfiguration() -> mapperElement()
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
// 扫描包下的Mapper
} else {
// 解析单个Mapper文件
String resource = child.getStringAttribute("resource");
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream</mark>, configuration, resource, configuration.getSqlFragments());
// 在这里创建二级缓存
mapperParser.parse();
}
}
}
}
- 在XMLMapperBuilder.configurationElement方法中,会解析Mapper中<cache>标签;
java
public class XMLMapperBuilder extends BaseBuilder {
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
}
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
// 解析cache标签,创建二级缓存实例
cacheElement(context.evalNode("cache"));
cacheRefElement(context.evalNode("cache-ref"));
// ...其他解析
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
}
- 解析缓存配置,准备创建实例

- 在MapperBuilderAssistant#useNewCache中

- org.apache.ibatis.mapping.CacheBuilder#build中,应用装饰模式

- 在Mapper.xml中,可配置size、clearInterval等属性,会添加相应的装饰器。

java
// CacheBuilder中的装饰器应用顺序
Cache cache = new PerpetualCache(id);
cache = new LruCache(cache); // 淘汰策略,默认Lru
cache = new ScheduledCache(cache); // 配置了clearInterval时
cache = new SerializedCache(cache); // 配置了readWrite时
cache = new LoggingCache(cache); // 日志
cache = new SynchronizedCache(cache); // 同步
cache = new BlockingCache(cache); // 配置了blocking时

2.2 使用
创建的二级缓存实例,会被对应的MappedStatement引用。
java
public class MappedStatement {
private Cache cache; // 引用对应的缓存实例
public Cache getCache() {
return cache;
}
}
CachingExecutor在查询时,从MappedStatement中获取二级缓存实例并使用
java
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// 获取Mapper配置文件中二级缓存实例
Cache cache = ms.getCache();
// 启用了二级缓存
if (cache != null) {
// 使用缓存
}
//未启用二级缓存,直接查询数据库
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

三 特别的TransactionalCache
TransactionalCache也是Cache接口的实现,是MyBatis二级缓存的事务缓冲区,为二级缓存提供事务语义支持。有以下特性
- 延迟写入
- 缓存操作不直接写入二级缓存
- 所有写操作暂存在entriesToAddOnCommit中
- 只有事务提交时才真正写入共享缓存
- 事务隔离
- 每个事务有独立的TransactionalCache实例
- 未提交的缓存项对其他事务不可见
- 保证缓存的事务一致性
java
private final Cache delegate;
private boolean clearOnCommit;
// 待提交的缓存项
private final Map<Object, Object> entriesToAddOnCommit;
// 缓存未命中的key。TransactionalCache.putObject不会触发BlockingCache释放锁,
// 因此rollback时对value为null的key,需要执行put来解锁
private final Set<Object> entriesMissedInCache;
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
// 提交时, 将待添加的缓存项添加到二级缓存中
flushPendingEntries();
reset();
}
// 回滚时,丢弃待添加的缓存项
public void rollback() {
// 回滚时, 解锁所有未命中缓存项的锁对象
unlockMissedEntries();
reset();
}
3.1 事务隔离
mybatis中二级缓存跨session共享,当使用TransactionalCache装饰时,如何区分不同事务的待提交缓存项呢?
答案是:不同事务的缓存操作完全隔离。MyBatis通过为每个SqlSession创建独立的TransactionalCache实例来实现事务级别的缓存隔离,而不是在单个实例内部区分不同事务。
-
实例级别隔离:每个SqlSession在使用二级缓存时,都会创建自己的TransactionalCache包装器
-
本地缓存区:每个TransactionalCache维护自己的entriesToAddOnCommit映射,存储该事务的待提交项
-
事务边界控制:只有在调用commit()时,待提交的缓存项才会真正写入共享的二级缓存。
事务A (SqlSession A) 事务B (SqlSession B)
| |
TransactionalCache A TransactionalCache B
| |
entriesToAddOnCommit entriesToAddOnCommit
{key1: value1} {key2: value2}
| |
commit() rollback()
| |
写入二级缓存 丢弃缓存项
注意,SqlSession非线程安全,不应被多线程并发使用;因此某一时刻下某个sqlSession最多处理一个事务。
3.2 源码实现
MyBatis通过实例级别的隔离而非数据级别的区分,来实现不同事务的TransactionalCache隔离。
- CachingExecutor持有TransactionalCacheManager实例;

- TransactionalCacheManager会为每个二级缓存创建一个TransactionalCache对象
java
// 二级缓存的TransactionalCache包装
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();
private TransactionalCache getTransactionalCache(Cache cache) {
TransactionalCache txCache = transactionalCaches.get(cache);
if (txCache == null) {
txCache = new TransactionalCache(cache);
transactionalCaches.put(cache, txCache);
}
return txCache;
}
事务隔离的实现路径
- SqlSession级别隔离:每个事务使用独立的SqlSession实例
- Executor级别隔离:每个SqlSession创建独立的CachingExecutor
- TransactionalCacheManager级别隔离:每个CachingExecutor有独立的TransactionalCacheManager
- TransactionalCache级别隔离:每个二级Cache在每个事务中都有独立的TransactionalCache包装