目录
mybatis 全局缓存解析
价值 | ★★☆ |
实用 | ★☆ |
基本概念
- LRU – 最近最少使用的:移除最长时间不被使用的对象。
- FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
- SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。
- WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象
疑问
本文要解决以下几个问题:
- 默认的缓存是什么
- 为什么 查询对象 == 缓存对象 的结果为 false,怎么让他们相等
- 总共有几种缓存类型
- 缓存的结构
- 如何注册自定义缓存,需要做什么
声明: 本文不对缓存的实现方式分析
例子
BlogMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.aya.mapper.BlogMapper">
<cache/>
<select id="selectAll" resultType="com.aya.mapper.Blog" >
select * from blog
</select>
</mapper>
mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="config.properties">
</properties>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="cache/global/readonly/UserMapper.xml"/>
</mappers>
</configuration>
测试类
@Test
public void testGlobalCacheReadOnly() throws IOException {
String resource = "cache/global/readonly/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
List<Object> objectsA = null;
List<Object> objectsB = null;
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
objectsA = sqlSession.selectList("selectAll");
} catch (Exception e) {
e.printStackTrace();
}
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
//命中全局缓存
objectsB = sqlSession.selectList("selectAll");
} catch (Exception e) {
e.printStackTrace();
}
Assert.assertTrue(objectsA == objectsB);
}
junit 测试返回失败
缓存解析
第一节 默认缓存
mapper 的 cache 节点解析
private void cacheElement(XNode context) throws Exception {
if (context != null) {
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();
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}
轩辕婉儿: 哇,好多啊,眼花缭乱的
二狗: -_-. 那怎么办?
轩辕婉儿: 我是谁? 我在哪儿? 上面的代码是怎么来的?
二狗: 这里mybatis是解析 mapper.xml 文件中, 解析节点 mapper/cache 的代码
轩辕婉儿: 看起来好多,都做了些什么啊?
二狗: 这里都是读取节点 cache 的属性,然后最后 useNewCache 添加到 configuration 里面
轩辕婉儿: 没了? 看见我手里的水火棍了没? 你这样讲谁能听懂啊!!!
二狗: 好吧,好吧,仔细分析一下
- 获得永久缓存对象 PERPETUAL,创建 typeClass
- 获得 eviction 的值,如果没有设置就是LRU, 创建 evictionClass
- 设置缓存边界 size, 例如最大只能存10个. 多了就删掉
- readWrite 是否要读写缓存, (后文用于判断是否需要序列化)
- 是否堵塞缓存(后文用户判断是否进行堵塞)
- 获得properties,设置缓存的属性
- 创建缓存
轩辕婉儿: 你TM在逗我? 刚刚一句话就讲完了,现在又说这么多?
二狗: 是你要仔细分析的啊,现在…
轩辕婉儿: 少废话, 不过好像有提到 默认缓存是LRU?
二狗: 对的,默认缓存就是LRU
String eviction = context.getStringAttribute("eviction", "LRU");
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
轩辕婉儿: 这里默认是LRU , 那我可不可以写: ABCD 来替换它
二狗: 不行
轩辕婉儿: 为什么? 为什么 LRU 可以用,我就不能随便写?
二狗: 那是因为 LRU 这是已经注册好的缓存类型,不是随便写的
轩辕婉儿: 那你告诉我 LRU 从哪里来的?
二狗: 这得从前天下午我喝下午茶的时候开始讲起, 那时候我正在晒太阳,突然冲出一条狗,我被吓一跳,
啪
轩辕婉儿: 二狗啊,皮痒了? 接着讲LRU从哪里来的
二狗: 哎呀,很痛的啊, 不要随便动粗知道吗?
轩辕婉儿: 我也不想啊,谁让你打岔
二狗: 好吧, 其实这很简单, LRU的Class对象是由 typeAliasRegistry.解析别名
获得,那么一定有一个typeAliasRegistry.注册别名
去注册 Class 对象,我们只要去找找它的注册别名的地方就一定能找到
轩辕婉儿: 好,那我们赶快去
第二节 缓存的四种类型
高尔夫球场旁的一条林荫小道,一阵热浪拂过,空气中一阵茉莉的芳香
在小河旁边有一套石凳,夕阳的余晖透过梧桐如繁星洒在地面,
二狗正在品味手里的八宝茶,婉儿坐在旁边。 好家伙,这两个家伙正在畅想未来呢
二狗: 咳咳咳,要开始了
轩辕婉儿: 啊,刚才讲到哪里了?
二狗: 要去找 注册别名
的地方了
轩辕婉儿: 有吗? 在哪儿呢?
二狗: 你别说,还真有,他就叫注册别名
public void registerAlias(String alias, Class<?> value) {
// 省略实现
}
轩辕婉儿: 那真是太好了,真的有呢。 可是我怎么知道哪里注册了缓存呢?
二狗: 这个简单,打开全局搜索: registerAlias("LRU"
我们找LRU注册的地方,就能找到其他缓存注册的地方了
轩辕婉儿: 哇,狗哥, 你开挂啊,还真是呢,一下子就找到了五个,哈哈哈
public Configuration() {
// 省略其他不是缓存的注册
typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class);
typeAliasRegistry.registerAlias("FIFO", FifoCache.class);
typeAliasRegistry.registerAlias("LRU", LruCache.class);
typeAliasRegistry.registerAlias("SOFT", SoftCache.class);
typeAliasRegistry.registerAlias("WEAK", WeakCache.class);
}
轩辕婉儿: 五个, 五~~个! 五个? 怎么是五个? 不是四个吗?
二狗:是四个啊
二狗: 别,别, 别, 快松手,耳朵要掉了
轩辕婉儿: 那你快说怎么回事
二狗: 确实是注册了五个缓存类型,但是 PERPETUAL 它是一个源,可以注册的四种都是用来装饰用的
轩辕婉儿: 你看到我脑袋上面的问号围成一圈了吗?
二狗: 这得去创建缓存的地方去看看能说清楚
轩辕婉儿: 走吧,二狗
第三节 缓存的结构
二狗: 这就要去分析: builderAssistant.useNewCache
里面的部分了,请看下面的实现
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;
}
二狗: 这里讲缓存分为两个部分, 如下图
- 实现: PerpetualCache(持久化缓存)
- 装饰: LruCache,日志,序列化, 等
轩辕婉儿: 哦,我明白了,核心只有一个,不可以修改,外面的随便怎么样的都可以.
二狗: 哇,婉儿,你好聪明,什么时候进化了?
二狗: 哟,哟,丝, 好了好了,别拧了
轩辕婉儿: 可是我想知道具体有哪些可以装饰的,还有这些装饰能不能同时存在呢?
二狗: 那就去看看 build 部分,看他具体是如何去实现和装饰的
public Cache build() {
setDefaultImplementations();
Cache cache = newBaseCacheInstance(implementation, id);
setCacheProperties(cache);
// issue #352, do not apply decorators to custom caches
// 标准装饰器
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;
}
二狗: 这里分为两个部分,这里我们只分析标准装饰
- 标准的装饰: 将 自定义的缓存 装饰 PerpetualCache, 在进行标准的装饰
- 自定义装饰,LoggingCache(自定义装饰器)
二狗: 那你知道 PerpetualCache 是如何创建 的吗?
轩辕婉儿: 恩,我看到了,这里不就是新建一个对象吗!! 根本没什么嘛. 那接下来呢?
Cache cache = newBaseCacheInstance(implementation, id);
相当于
Cache cache = new PerpetualCache(id);
二狗: 接下来 foreach 循环遍历 decorators, 那么缓存就变成了: LruCache(PerpetualCache)
二狗: 再接下来 setStandardDecorators 设置标准的装饰,这里的装饰器才多呢
轩辕婉儿: 恩,那我们去看看里面有什么吧
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);
}
}
轩辕婉儿: 哇,狗哥,狗哥,这里都是些什么啊,赶快跟我解释解释吧
二狗: 好嘞,
- 配置clearInterval时, 调度器缓存装饰器 ScheduledCache(customCache)
- 配置readOnly=false(默认为false)时, 序列化缓存装饰器 SerializedCache(customCache)
- 日志缓存装饰器
- 同步缓存装饰器
- 配置 blocking=true(默认为false)时, 阻塞缓存装饰器
最后变成了这个样子: BlockingCache?(SynchronizedCache(LoggingCache(SerializedCache?(ScheduledCache?(LruCache(PerpetualCache))))))
轩辕婉儿: 太复杂了,能不能画个图?
二狗: 末将领命!!!
- 符号
?
表示可有可无 - 符号
|
表示多选一 - 未加符号表示必须存在
轩辕婉儿: 真的好清晰呢,这么多装饰器都可以同时存在啊,那我能不能自定义缓存像FIFO,LRU 那样的。
二狗: 不能,如果你自定义的话,那就走自定义缓存处理流程了。
二狗: 那你现在知道为什么 例子中的objectsA == objectsB 失败了吗?
轩辕婉儿: 啊? 我不知道啊,为什么啊?
二狗: 那是因为有序列化缓存啊, 序列化读取出来的对象 和 原始对象的地址一定不一样
轩辕婉儿: 那照你的意思? 关掉序列化缓存就可以了咯?
二狗: 恩,没错
轩辕婉儿: 那怎么关掉呢?
二狗: 这里有三个是可以通过开关关掉的。其中 readOnly=true ,就是关掉序列化缓存的开关了
轩辕婉儿: 那还有两个怎么关?
二狗: 另外两个不用关, 他们默认就没打开
- ScheduledCache, 通过 flushInterval=
10000
默认关闭 - SerializedCache 通过readOnly=true打开,默认打开
- BlockingCache 通过 blocking=true 打开,默认关闭
轩辕婉儿: 狗哥,狗哥,你真是太厉害了。 原来mybatis的缓存是这样的啊.
自定义缓存
轩辕婉儿: 二狗啊,那自定义缓存是怎么回事啊? 给本宫解释解释
二狗: 自定义缓存啊,就是实现了 Cache 接口,然后加入日志装饰器的缓存
轩辕婉儿: 那流程完全不一样啊
二狗: 恩,他的流程更加简单一些了
轩辕婉儿: 那有没有什么需要注意的地方呢?
二狗: 当然有啦,这里的缓存不加锁,不再是线程安全的了,而且没有了调度器会一直在内存里面,不会清空了,等等…
轩辕婉儿: 那还是太麻烦了,还是用默认LRU就好了
二狗: 恩, 当然可以。
轩辕婉儿: 那我们最后在加入一个简单的自定义缓存实例吧
自定义缓存的例子
public class DogCustomCache implements Cache {
String id ;
// 必须要有String 参数的构造器,
// Cache cache = newBaseCacheInstance(implementation, id); 会调用
public DogCustomCache(String id) {
this.id = id;
}
Map<Object,Object> map = new ConcurrentHashMap<>();
@Override
public String getId() {
return id;
}
@Override
public void putObject(Object key, Object value) {
map.put(key,value);
}
// 省略其他的 map 的代理方法
}
然后再Mapper.xml里面设置cache节点的type
<cache type="com.aya.cache.DogPerCustomCache"/>
总结
- 默认的缓存是LRU
- 设置readOnly=true,查询对象 == 缓存对象 的结果为 true
- 总共有四种缓存类型,LRU,FIFO,SOFT,WEAK
- 自定义需要实现 Cache 接口
参考
官网说明: http://www.mybatis.org/mybatis-3/zh/sqlmap-xml.html#cache