二级缓存
为方便理解,本章涉及示例代码已上传至 gitee
==>获取示例代码请点击这里。。。
拉取示例代码时,请拉取所有分支,master 分支只是做了示例的初始化
MyBatis 的二级缓存的种类大致可以分为三种:
- MyBatis 框架默认的二级缓存实现
- Ehcache 实现二级缓存
- Redis 实现二级缓存
在上一小节中提到过 MyBatis 中是通过 cacheEnabled 属性来设置是否启用二级缓存的,默认该值为 true ,即启用,接下来通过一张图来理解一下 MyBatis 对二级缓存是如何实现一步一步配置的:
从上图可以看出,XML 形式的配置和 注解 形式的配置最后都会通过 MapperBuilderAssistant#useNewCache(…) 完成对二级缓存配置的加载,我们来看下该方法的源码:
MapperBuilderAssistant.class
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)
// 具体的二级缓存实现类;如果在配置文件中没有设置二级缓存具体实现,则默认使用 PerpetualCache ;它也是一级缓存的实现方式
.implementation(valueOrDefault(typeClass, PerpetualCache.class))
// 设置 MyBatis 二级缓存的缓存策略
.addDecorator(valueOrDefault(evictionClass, LruCache.class))
// 二级缓存刷新时间间隔
.clearInterval(flushInterval)
// 二级缓存的长度
.size(size)
// 如果设置为 true 则表示 无论是存入或者获取,均经过序列化处理,可避免人为产生脏数据
// 为 false 时,则表示缓存中的数据为只读的
.readWrite(readWrite)
.blocking(blocking)
.properties(props)
// 稍后我们再看这个方法
.build();
configuration.addCache(cache);
currentCache = cache;
return cache;
}
下面来看 build() 方法:
CacheBuilder.class
public Cache build() {
setDefaultImplementations();
Cache cache = newBaseCacheInstance(implementation, id);
setCacheProperties(cache);
// 只有当使用 MyBatis 默认的二级缓存实现时,才会进行缓存策略的配置
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())) {
// 所有的二级缓存实现均经过 LoggingCache 装饰;该装饰器只做一件事:当从二级缓存中获取到数据时,打印缓存命令率
cache = new LoggingCache(cache);
}
return cache;
}
上面两个代码段为 MyBatis 二级缓存的通用步骤方法,下面依次介绍 MyBatis 常用的三种二级缓存实现:
MyBatis 框架默认的二级缓存实现
通过上面的通用步骤的源码,关于默认的二级缓存实现,我们只需关注 build() 方法中的 if 判断部分:
CacheBuilder.class
public Cache build() {
...
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())) {
// 所有的二级缓存实现均经过 LoggingCache 装饰;该装饰器只做一件事:当从二级缓存中获取到数据时,打印缓存命令率
cache = new LoggingCache(cache);
}
return cache;
}
上述 if 判断中,对默认二级缓存实现进行完缓存策略装饰之后,还会对缓存对象进行标准装饰,如下:上述 if 判断中,对默认二级缓存实现进行完缓存策略装饰之后,还会对缓存对象进行标准装饰,如下:
CacheBuilder.class
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);
}
if (readWrite) {
// 序列化缓存装饰器
cache = new SerializedCache(cache);
}
// 日志缓存装饰器
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);
}
}
配置二级缓存信息之后,便会在后续的解析对应的 SQL 语句时,与其绑定,大致流程图如下:
通过上面的一些介绍,现在大致清楚了 MyBatis 默认二级缓存的实现过程,现在通过一段示例代码,来了解一些具体使用:
SysUserMapper.xml
<cache eviction="FIFO" flushInterval="60000" size="5000" readOnly="false"/>
由于 MyBatis 的配置中 cacheEnabled 默认为 true,即使用二级缓存,同时,由于我们在这里设置了 readOnly 为 false,即表示从缓存中获取的内容可读写,所以,我们还需要让实体类实现 Serializable 接口,做完这些后,便可以使用 MyBatis 默认的二级缓存了,下面是我的测试方法:
@Test
public void testMyBatisDefaultL2Cache() throws Exception{
SqlSession sqlSession = sqlSessionFactory.openSession();
System.err.println("执行第一遍查询~~~~");
SysUserMapper mapper = sqlSession.getMapper(SysUserMapper.class);
mapper.selectById(1006);
// 关闭 sqlSession 为了验证:二级缓存不是基于 session 的
sqlSession.close();
System.err.println("重新启动一个 sqlSession ~~~~");
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SysUserMapper mapper1 = sqlSession1.getMapper(SysUserMapper.class);
System.err.println("执行第二遍查询~~~~");
mapper1.selectById(1006);
sqlSession1.close();
// 重现创建一个 SqlSessionFactory 对象,为了验证:二级缓存时基于 SqlSessionFactory 的
Reader resourceAsReader = Resources.getResourceAsReader("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory1 = new SqlSessionFactoryBuilder().build(resourceAsReader);
resourceAsReader.close();
SqlSession sqlSession2 = sqlSessionFactory1.openSession();
SysUserMapper mapper2 = sqlSession2.getMapper(SysUserMapper.class);
System.err.println("执行第三遍查询~~~~");
mapper2.selectById(1006);
}
执行此测试方法,控制台打印如下:
执行第一遍查询~~~~
// 第一次查询,未命中缓存 0.0
DEBUG [main] - Cache Hit Ratio [com.mybatis.simple.mapper.SysUserMapper]: 0.0
DEBUG [main] - ==> Preparing: select * from sys_user where id = ?
DEBUG [main] - ==> Parameters: 1006(Integer)
TRACE [main] - <== Columns: id, user_name, user_password, user_email, user_info, head_img, create_time
TRACE [main] - <== Row: 1006, 孙悟空, 123456, SWK@xiyouji.com, <<BLOB>>, <<BLOB>>, 2020-06-15 07:46:06
DEBUG [main] - <== Total: 1
重新启动一个 sqlSession ~~~~
执行第二遍查询~~~~
// 第二次查询,命中缓存 0.5
DEBUG [main] - Cache Hit Ratio [com.mybatis.simple.mapper.SysUserMapper]: 0.5
执行第三遍查询~~~~
// 第三次查询,由于 SqlSessionFactory 对象为新创建的,故此,二级缓存未命中;结论:MyBatis 默认二级缓存生命周期基于 SqlSessionFactory 而存在
DEBUG [main] - Cache Hit Ratio [com.mybatis.simple.mapper.SysUserMapper]: 0.0
DEBUG [main] - ==> Preparing: select * from sys_user where id = ?
DEBUG [main] - ==> Parameters: 1006(Integer)
TRACE [main] - <== Columns: id, user_name, user_password, user_email, user_info, head_img, create_time
TRACE [main] - <== Row: 1006, 孙悟空, 123456, SWK@xiyouji.com, <<BLOB>>, <<BLOB>>, 2020-06-15 07:46:06
DEBUG [main] - <== Total: 1
Process finished with exit code 0
我们看到,在执行第一遍查询时,首先查询二级缓存,缓存未命中,打印值为:0.0,回去查询数据库,查询完后将结果存入分别存入一级、二级缓存,当我们完成第一遍查询后,关闭 session ,由于一级缓存是基于 session 存在的,所以,**再次打开一个 session 时,原一级缓存失效,执行第二遍查询,命中缓存,控制台打印缓存命中率为:0.5;**因为共查询了两次,第一次未命中,第二次命中。执行完第二次查询,我们关闭 session ,重新创建一个 SqlSessionFactory 对象,再次执行查询,可以看到,控制台打印处未命中二级缓存,命中率打印:0.0;重新从查询数据库。
下一篇来说说 《MyBatis 的 基于 JVM 虚拟机存储的 Ehcache 缓存》