作者:京东科技 王奕龙
1. 验证二级缓存
在上一篇帖子中的 User 和 Department 实体类依然要用,这里就不再赘述了,要启用二级缓存,需要在 Mapper.xml 文件中指定 cache 标签,如下:
sql
UserMapper.xml
<select id="findAll" resultType="User">
select * from user
</select>
<cache />
ini
Department.xml
<select id="findAll" resultType="entity.Department">
select * from department;
</select>
<cache readOnly="true"/>
在 Department.xml 中的 cache 标签指定了 readOnly 属性,因为该配置相对比较重要,所以我们在这里把它讲解一下:
readOnly 默认为 false ,这种情况下通过二级缓存查询出来的数据会进行一次 序列化深拷贝 。在这里大家需要回想一下介绍一级缓存时举的例子:一级缓存查询出来返回的是 该对象的引用 ,若我们对它修改,再查询 时触发一级缓存获得的便是 被修改过的数据 。但是,二级缓存的序列化机制则不同,它获取到的是 缓存深拷贝的对象,这样对二级缓存进行修改操作不影响后续查询结果。
如果将该属性配置为 true 的话,那么它就会变得和一级缓存一样,返回的是对象的引用,这样做的好处是 避免了深拷贝的开销。
为什么会有这种机制呢?
因为二级缓存是 Mapper级别 的,不能保证其他 SqlSession 不对二级缓存进行修改,所以这也是一种保护机制。
我们验证一下这个例子,Department 和 User 的查询都执行了两遍(注意 事务提交之后 才能使二级缓存生效):
ini
public static void main(String[] args) {
InputStream xml = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
// 开启二级缓存需要在同一个SqlSessionFactory下,二级缓存存在于 SqlSessionFactory 生命周期,如此才能命中二级缓存
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(xml);
SqlSession sqlSession1 = sqlSessionFactory.openSession();
UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
DepartmentMapper departmentMapper1 = sqlSession1.getMapper(DepartmentMapper.class);
System.out.println("----------department第一次查询 ↓------------");
List<Department> departments1 = departmentMapper1.findAll();
System.out.println("----------user第一次查询 ↓------------");
List<User> users1 = userMapper1.findAll();
// 提交事务,使二级缓存生效
sqlSession1.commit();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
DepartmentMapper departmentMapper2 = sqlSession2.getMapper(DepartmentMapper.class);
System.out.println("----------department第二次查询 ↓------------");
List<Department> departments2 = departmentMapper2.findAll();
System.out.println("----------user第二次查询 ↓------------");
List<User> users2 = userMapper2.findAll();
sqlSession1.close();
sqlSession2.close();
}
Department 和 User 的同一条查询语句都执行了两遍,因为 Department 指定了 readOnly 为true,那么 两次查询返回的对象均为同一个引用,而 User 则反之,Debug 试一下:

cache 的其他属性
属性 | 描述 | 备注 |
---|---|---|
eviction | 缓存回收策略 | 默认 LRU |
type | 二级缓存的实现类 | 默认实现 PerpetualCache |
size | 缓存引用数量 | 默认1024 |
flushInterval | 定时清除时间间隔 | 默认无 |
blocking | 阻塞获取缓存数据 | 若缓存中找不到对应的 key ,是否会一直阻塞,直到有对应的数据进入缓存。默认 false |
接下来我们测试验证下二级缓存的生效:
ini
SqlSession sqlSession1 = sqlSessionFactory.openSession();
DepartmentMapper departmentMapper1 = sqlSession1.getMapper(DepartmentMapper.class);
System.out.println("----------department第一次查询 ↓------------");
List<Department> departments1 = departmentMapper1.findAll();
// 使二级缓存生效
sqlSession1.commit();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
DepartmentMapper departmentMapper2 = sqlSession2.getMapper(DepartmentMapper.class);
System.out.println("----------department第二次查询 ↓------------");
List<Department> departments2 = departmentMapper2.findAll();
- 第一次 Query,会去数据库中查
- 第二次 Query,直接从二级缓存中取
2. 二级缓存的原理
二级缓存对象 Cache
在加载 Mapper 文件(org.apache.ibatis.builder.xml.XMLConfigBuilder#mappersElement
方法)时,定义了加载 cache 标签的步骤(org.apache.ibatis.builder.xml.XMLMapperBuilder#configurationElement
方法),代码如下:
scala
public class XMLMapperBuilder extends BaseBuilder {
// ...
private void configurationElement(XNode context) {
try {
// 若想要在多个命名空间中共享相同的缓存配置和实例,可以使用 cache-ref 元素来引用另一个缓存
cacheRefElement(context.evalNode("cache-ref"));
// 配置二级缓存
cacheElement(context.evalNode("cache"));
// ...
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
}
具体解析逻辑如下:
ini
public class XMLMapperBuilder extends BaseBuilder {
// ...
private void cacheElement(XNode context) {
if (context != null) {
// 二级缓存实现类,默认 PerpetualCache,我们在一级缓存也提到过
String type = context.getStringAttribute("type", "PERPETUAL");
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
// 缓存清除策略,默认 LRU
String eviction = context.getStringAttribute("eviction", "LRU");
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
// 定时清除间隔
Long flushInterval = context.getLongAttribute("flushInterval");
// 缓存引用数量
Integer size = context.getIntAttribute("size");
// readOnly上文我们提到过,默认 false
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
// blocking 默认 false
boolean blocking = context.getBooleanAttribute("blocking", false);
Properties props = context.getChildrenAsProperties();
// 创建缓存对象
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}
}
我们继续看创建二级缓存对象的逻辑 org.apache.ibatis.builder.MapperBuilderAssistant#useNewCache
,可以发现,创建 Cache 对象使用了 建造者模式:

建造者 CacheBuilder
并没有被组合在任意一种缓存的实现类中,而是根据如下代码中 implementation(valueOrDefault(typeClass, PerpetualCache.class))
逻辑指定了要创建的缓存类型,并在 build
方法中使用反射创建对应实现类:
scala
public class MapperBuilderAssistant extends BaseBuilder {
// ...
public Cache useNewCache(Class<? extends Cache> typeClass, Class<? extends Cache> evictionClass, Long flushInterval,
Integer size, boolean readWrite, boolean blocking, Properties props) {
// 建造者模式,将标签属性赋值
Cache cache = new CacheBuilder(currentNamespace).implementation(valueOrDefault(typeClass, PerpetualCache.class))
.addDecorator(valueOrDefault(evictionClass, LruCache.class)).clearInterval(flushInterval).size(size)
.readWrite(readWrite).blocking(blocking).properties(props).build();
// 添加到全局配置中
configuration.addCache(cache);
currentCache = cache;
return cache;
}
}
其中 addDecorator(valueOrDefault(evictionClass, LruCache.class))
逻辑添加了 装饰器 ,使用了 装饰器模式 ,将 LruCache
类型的装饰器添加到 decorators
中:
kotlin
public class CacheBuilder {
private final List<Class<? extends Cache>> decorators;
public CacheBuilder addDecorator(Class<? extends Cache> decorator) {
// 将 LruCache 装饰器添加到 decorators
if (decorator != null) {
this.decorators.add(decorator);
}
return this;
}
// ...
}
在 CacheBuilder#build
方法中,如下为封装装饰器的逻辑:
scss
public class CacheBuilder {
// ...
public Cache build() {
setDefaultImplementations();
// 反射创建 PerpetualCache
Cache cache = newBaseCacheInstance(implementation, id);
setCacheProperties(cache);
// 封装装饰器的逻辑
if (PerpetualCache.class.equals(cache.getClass())) {
for (Class<? extends Cache> decorator : decorators) {
cache = newCacheDecoratorInstance(decorator, cache);
setCacheProperties(cache);
}
// 初始化基础必要的装饰器
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);
}
// 定时清空二级缓存
if (clearInterval != null) {
cache = new ScheduledCache(cache);
((ScheduledCache) cache).setClearInterval(clearInterval);
}
// readOnly属性相关的读写缓存
if (readWrite) {
cache = new SerializedCache(cache);
}
// 日志缓存和同步缓存(借助 ReentrantLock 实现)
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);
}
}
}
所有装饰器都在 org.apache.ibatis.cache.decorators
包下,唯独 PerpetualCache
在org.apache.ibatis.cache.impl
包下:

PerpetualCache
中不包含 delegate
属性表示装饰器,说明它将作为最基础的实现类被其他装饰器装饰,而其他装饰器中均含有 delegate
属性来装饰其他实现。
默认创建的二级缓存类型如下:

类关系图如下:

query 方法对二级缓存的应用
org.apache.ibatis.executor.CachingExecutor#query
方法使用了二级缓存,如下代码所示:
java
public class CachingExecutor implements Executor {
// 事务缓存管理器
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// 先获取二级缓存,该对象便是上文中创建的被装饰器装饰的 PerpetualCache
Cache cache = ms.getCache();
if (cache != null) {
// 判断是否需要清除缓存
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
// 从二级缓存中取
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
// 没取到二级缓存,尝试取一级缓存或去数据库查询
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// "添加二级缓存"
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
// 没有二级缓存的话,执行的是我们在一级缓存中介绍的方法,要么取一级缓存,否则去数据库查
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
// ...
}
上述逻辑比较清晰,我们在上文中提到过,只有 事务提交的时候才会将二级缓存保存 ,但是其中有 tcm.putObject(cache, key, list);
逻辑,似乎在这里保存了二级缓存,而此时事务还未提交,这便需要我们一探究竟。它会执行到 TransactionalCacheManager#putObject
方法:
typescript
public class TransactionalCacheManager {
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
public void putObject(Cache cache, CacheKey key, Object value) {
getTransactionalCache(cache).putObject(key, value);
}
private TransactionalCache getTransactionalCache(Cache cache) {
return MapUtil.computeIfAbsent(transactionalCaches, cache, TransactionalCache::new);
}
}
TransactionalCacheManager
事务缓存管理器会创建并管理 TransactionalCache
对象,TransactionalCache
同样是 Cache
装饰器,它将装饰在 SynchronizedCache
上:
typescript
public class TransactionalCache implements Cache {
// 被装饰对象,默认是 SynchronizedCache
private final Cache delegate;
// 该元素将保存在事务 commit 时被保存的键值对缓存
private final Map<Object, Object> entriesToAddOnCommit;
@Override
public void putObject(Object key, Object object) {
entriesToAddOnCommit.put(key, object);
}
// ...
}
putObject
执行时便是向 entriesToAddOnCommit
添加元素,记录二级缓存键值对,并没有真正添加到二级缓存 PerpetualCache
对象中。此外,entriesToAddOnCommit
的命名,也暗示了在事务提交时缓存才会被保存。那么接下来,便需要看一下事务提交逻辑。
在上文测试二级缓存的代码中,有 sqlSession1.commit();
逻辑。在事务提交时,它会走到 CachingExecutor#commit
方法,其中会调用到 TransactionalCacheManager#commit
方法,如下:
java
public class CachingExecutor implements Executor {
// ...
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
@Override
public void commit(boolean required) throws SQLException {
// ...
tcm.commit();
}
}
在该方法中,会遍历所有的事务缓存 TransactionalCache
,并逐一调用它们的 commit
方法,
typescript
public class TransactionalCacheManager {
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}
// ...
commit
方法会调用 delegate.commit
方法,而 delegate
为被装饰对象,最后便会将二级缓存记录:
typescript
public class TransactionalCache implements Cache {
private final Map<Object, Object> entriesToAddOnCommit;
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
flushPendingEntries();
reset();
}
private void flushPendingEntries() {
// 事务提交,将 entriesToAddOnCommit 中所有待添加的二级缓存添加
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}
private void reset() {
clearOnCommit = false;
entriesToAddOnCommit.clear();
entriesMissedInCache.clear();
}
// ...
}
缓存失效
事务回滚是不是会使本次事务中相关的二级缓存失效呢?
typescript
public class TransactionalCache implements Cache {
public void rollback() {
unlockMissedEntries();
reset();
}
private void reset() {
clearOnCommit = false;
entriesToAddOnCommit.clear();
entriesMissedInCache.clear();
}
private void unlockMissedEntries() {
for (Object entry : entriesMissedInCache) {
try {
delegate.removeObject(entry);
} catch (Exception e) {
log.warn("Unexpected exception while notifying a rollback to the cache adapter. "
+ "Consider upgrading your cache adapter to the latest version. Cause: " + e);
}
}
}
// ...
}
的确如此,它会将未被缓存的元素清除 reset()
,也会把在本次事务中操作过的数据在二级缓存中移除 unlockMissedEntries()
。
那数据发生新增、修改或删除呢?同样会清除缓存
java
public class CachingExecutor implements Executor {
@Override
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
flushCacheIfRequired(ms);
return delegate.update(ms, parameterObject);
}
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
// 默认 flushCacheRequired 为 true
if (cache != null && ms.isFlushCacheRequired()) {
tcm.clear(cache);
}
}
它将调用 TransactionalCache#clear
方法,将待生效的 entriesToAddOnCommit
二级缓存清除,并标记 clearOnCommit
为 true,在事务提交时,二级缓存会执行清除缓存的 clear
方法:
scss
@Override
public void clear() {
clearOnCommit = true;
entriesToAddOnCommit.clear();
}
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
flushPendingEntries();
reset();
}
缓存生效范围
到这里,我们已经基本弄清楚二级缓存生效的原理了,那么接下来我们需要解释"为什么二级缓存是 Mapper 级别的?"其实也非常简单,看如下代码:
java
public class CachingExecutor implements Executor {
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// 先获取二级缓存,该对象便是上文中创建的被装饰器装饰的 PerpetualCache
Cache cache = ms.getCache();
// ...
}
// ...
}
在执行查询时,二级缓存 Cache
是在 MappedStatement
中获取的,Mapper 中每个 SQL 声明都对应唯一的 MappedStatement
,当同一条 SQL 被执行时,它们都会去取同样的缓存,所以可以说它是 Mapper 级别的,说成 MappedStatement
级别更准确,二级缓存支持多个 SqlSession
共享。
为什么要在事务提交后才生效?
在这里我们讨论一个问题:为什么二级要在事务提交后才能生效呢?
因为二级缓存可以在不同的 SqlSession
间生效,画个图你就明白了:

如果 SqlSession1先修改了数据 ,再查询数据 ,如果二级缓存在事务未提交时就生效,那么 SqlSession2 调用同样的查询时便会从 二级缓存中获取数据 ,但是此时 SqlSession1回滚了事务 ,那么此时就会导致 SqlSession2 从二级缓存获取的数据 变成脏数据,这就是为什么二级缓存要在事务提交后才能生效的原因。
3. 为什么要扩展二级缓存?
MyBatis 中设计一级缓存和二级缓存的目的是为了提高数据库访问的效率,但它们的作用范围和使用场景有所不同,各自有其特定的用途和优势。
一级缓存 默认开启,是基于 SqlSession
的,也就是说,它的作用范围仅限于一次数据库会话,所以当会话关闭后,缓存就会被清除。这意味着不同会话之间无法共享缓存数据。而 二级缓存 是基于 Mapper 级别的,需要显式配置开启,可以在多个 SqlSession
之间共享。当然也由于二级缓存的作用范围更广,因此需要更复杂的缓存失效策略和数据一致性管理,以避免数据不一致的问题。二级缓存的引入是为了在更大范围内(多个会话之间)提高数据访问的效率,特别是在读多写少的应用场景。
4. 总结
- 二级缓存本质上是
HashMap
,在PerpetualCache
实现类中 - 二级缓存是 Mapper 级别的,可以在不同
SqlSession
间共享 - 特殊的 readOnly 标签,默认为 false,表示二级缓存中是被深拷贝的对象
- 二级缓存需要在事务提交后才能生效
- 执行 Insert、Delete、Update 语句会使 当前 Mapper 下的二级缓存失效