mybatis 全局缓存解析

目录

mybatis 全局缓存解析

价值★★☆
实用★☆

基本概念

  • LRU – 最近最少使用的:移除最长时间不被使用的对象。
  • FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
  • SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。
  • WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象

疑问

本文要解决以下几个问题:

  1. 默认的缓存是什么
  2. 为什么 查询对象 == 缓存对象 的结果为 false,怎么让他们相等
  3. 总共有几种缓存类型
  4. 缓存的结构
  5. 如何注册自定义缓存,需要做什么

声明: 本文不对缓存的实现方式分析

例子

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 里面

轩辕婉儿: 没了? 看见我手里的水火棍了没? 你这样讲谁能听懂啊!!!

二狗: 好吧,好吧,仔细分析一下

  1. 获得永久缓存对象 PERPETUAL,创建 typeClass
  2. 获得 eviction 的值,如果没有设置就是LRU, 创建 evictionClass
  3. 设置缓存边界 size, 例如最大只能存10个. 多了就删掉
  4. readWrite 是否要读写缓存, (后文用于判断是否需要序列化)
  5. 是否堵塞缓存(后文用户判断是否进行堵塞)
  6. 获得properties,设置缓存的属性
  7. 创建缓存

轩辕婉儿: 你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;
  }

二狗: 这里讲缓存分为两个部分, 如下图

  1. 实现: PerpetualCache(持久化缓存)
  2. 装饰: 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;
  }

二狗: 这里分为两个部分,这里我们只分析标准装饰

  1. 标准的装饰: 将 自定义的缓存 装饰 PerpetualCache, 在进行标准的装饰
  2. 自定义装饰,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);
    }
  }

轩辕婉儿: 哇,狗哥,狗哥,这里都是些什么啊,赶快跟我解释解释吧

二狗: 好嘞,

  1. 配置clearInterval时, 调度器缓存装饰器 ScheduledCache(customCache)
  2. 配置readOnly=false(默认为false)时, 序列化缓存装饰器 SerializedCache(customCache)
  3. 日志缓存装饰器
  4. 同步缓存装饰器
  5. 配置 blocking=true(默认为false)时, 阻塞缓存装饰器

最后变成了这个样子: BlockingCache?(SynchronizedCache(LoggingCache(SerializedCache?(ScheduledCache?(LruCache(PerpetualCache))))))

轩辕婉儿: 太复杂了,能不能画个图?

二狗: 末将领命!!!

  1. 符号 ? 表示可有可无
  2. 符号 | 表示多选一
  3. 未加符号表示必须存在
    缓存装饰器全图

轩辕婉儿: 真的好清晰呢,这么多装饰器都可以同时存在啊,那我能不能自定义缓存像FIFO,LRU 那样的。

二狗: 不能,如果你自定义的话,那就走自定义缓存处理流程了。

二狗: 那你现在知道为什么 例子中的objectsA == objectsB 失败了吗?

轩辕婉儿: 啊? 我不知道啊,为什么啊?

二狗: 那是因为有序列化缓存啊, 序列化读取出来的对象 和 原始对象的地址一定不一样

轩辕婉儿: 那照你的意思? 关掉序列化缓存就可以了咯?

二狗: 恩,没错

轩辕婉儿: 那怎么关掉呢?

二狗: 这里有三个是可以通过开关关掉的。其中 readOnly=true ,就是关掉序列化缓存的开关了

轩辕婉儿: 那还有两个怎么关?

二狗: 另外两个不用关, 他们默认就没打开

  1. ScheduledCache, 通过 flushInterval=10000 默认关闭
  2. SerializedCache 通过readOnly=true打开,默认打开
  3. 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"/>

总结

  1. 默认的缓存是LRU
  2. 设置readOnly=true,查询对象 == 缓存对象 的结果为 true
  3. 总共有四种缓存类型,LRU,FIFO,SOFT,WEAK
  4. 自定义需要实现 Cache 接口

参考

官网说明: http://www.mybatis.org/mybatis-3/zh/sqlmap-xml.html#cache

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值