简约而不简单的本地缓存解决方案

Jodd导览

1. 导言

  • 缓存解决方案一般都具备几个核心功能:存值,取值,设置过期时间,值淘汰策略,命中率统计等。
  • 说到本地缓存解决方案,目前较为有名的该数Google的guava框架中的缓存了,不可否认,Google的缓存解决方案首先是金字塔顶端的程序员开发,且经过了Google数年项目实战。但是guava框架晦涩难懂的编码规范和大量使用设计模式,给想一探究竟的同学们带来了极大的困难。
  • Jodd Cache是开源项目Jodd中核心模块core中的一个子模块,运用Java中的常用数据结构、线程锁和设计模式构建了一套轻巧且功能强大的缓存框架,其源码相比于guava更容易阅读。下面,就跟着笔者一探究竟吧。

2. 缓存体系中常用的淘汰策略

参考:https://www.cnblogs.com/s-b-b/p/6047954.html

2.1. FIFO

First In First Out 先进先出策略,典型的队列特性。

  • 采用双向链表存储数据
  • 新加入的数据存放在队尾
  • 当队列满载时,将队首的元素剔除来释放空间

2.2. LRU

Least Recently Used 最近最少使用策略,该算法的核心思想是“如果数据最近被访问,那么将来被访问的概率也很大”。

  • 新加入数据存放在队尾位置
  • 每当某条缓存命中后,将该条数据移动到队尾位置
  • 当缓存满载后,删除队首数据释放空间

2.3. LFU

Least Frequently Used 使用频次策略,该算法的核心思想是“如果数据的历史访问次数高,那么将来被访问的概率也大”。

  • 新加入数据存放在队尾,此时初始命中次数为1
  • 当数据被访问命中时,命中次数加1,且触发队列元素重排序,队首命中次数最高
  • 当队列满载时,直接清除队尾元素释放空间

3. 实战

3.1. FIFOCache

  • 存、取和过期测试:

      @Test  
      public void cacheTest() throws InterruptedException {  
        // 设置容量为1,默认过期时长为2秒的FIFOCache  
        FIFOCache<String, String> cache = new FIFOCache<>(1, 2000); ① 
    
        cache.put("name", "zhangsan");// 默认2秒过期时间
        LOGGER.info("1. name = {}", cache.get("name"));// 未过期   
        TimeUnit.SECONDS.sleep(1); // ②
        LOGGER.info("2. name = {}", cache.get("name"));// 未过期   
        TimeUnit.SECONDS.sleep(3); // ③
        LOGGER.info("3. name = {}", cache.get("name"));// 已过期   
    
        cache.put("age", "3", 5000);// ④指定过期时间5秒  
        LOGGER.info("4. age = {}", cache.get("age"));// 未过期  
        TimeUnit.SECONDS.sleep(4);// ⑤  
        LOGGER.info("5. age = {}", cache.get("age"));// 未过期  
        TimeUnit.SECONDS.sleep(6);// ⑥  
        LOGGER.info("6. age = {}", cache.get("age"));// 已过期  
      }
    

    代码解析:

    • 代码①处创建了一个容量为1,默认过期时间是2000毫秒的FIFOCache缓存数据结构
    • 代码②处,由于缓存"name"是采用默认过期时间,所以睡眠1秒后数据未过期
    • 代码③处,只要被get命中一次,过期时间被重置,所以睡眠3秒后缓存过期
    • 代码处新增缓存"age",且手动指定过期时间是5000毫秒
    • 代码⑤处睡眠4秒,数据未过期
    • 代码⑥处睡眠6秒,数据已过期

    输出:

      code.xxxxxxx.com.jodd.JoddTest - 1. name = zhangsan
      code.xxxxxxx.com.jodd.JoddTest - 2. name = zhangsan
      code.xxxxxxx.com.jodd.JoddTest - 3. name = null
      code.xxxxxxx.com.jodd.JoddTest - 4. age = 3
      code.xxxxxxx.com.jodd.JoddTest - 5. age = 3
      code.xxxxxxx.com.jodd.JoddTest - 6. age = null
    
  • 淘汰策略测试

      @Test  
      public void pruneCacheTest(){  
          FIFOCache<String, String> cache = new FIFOCache<>(1);// ①  
    
        cache.put("name", "zhangsan");// ②
        LOGGER.info("1. name = {}", cache.get("name"));  
    
        cache.put("age", "3");// ③  
        LOGGER.info("2. name = {}", cache.get("name"));// ④  
        LOGGER.info("3. age = {}", cache.get("age"));  
      }
    

    代码解析:

    • 代码①处,创建容量为1,且不会过期的FIFOCache缓存数据结构
    • 代码②处将"name"放入缓存
    • 代码③处将"age"放入缓存
    • 代码④处,由于容量为1,name被淘汰,所以为null,从而也验证FIFO的淘汰策略

    输出:

      code.xxxxxxx.com.jodd.JoddTest - 1. name = zhangsan
      code.xxxxxxx.com.jodd.JoddTest - 2. name = null
      code.xxxxxxx.com.jodd.JoddTest - 3. age = 3
    

3.2. LRUCache

  • 存、取和过期测试:

      此测试流程与FIFOCache一致,不再做演示
    
  • 淘汰策略测试

      @Test  
      public void pruneCacheTest(){  
        LRUCache cache = new LRUCache(3);// 创建容量为3,永不过期的缓存  
        cache.put("a", "value-a");  // ①
        cache.put("b", "value-b");  
        cache.put("c", "value-c");  
    
        cache.get("a");// a命中  // ②
        cache.get("b");// b命中  
    
        LOGGER.info("------{}------", "c被淘汰");  
        cache.put("d", "value-d");// ③
        showCache(cache);// 显示缓存列表,c被淘汰  
    
        LOGGER.info("------{}------", "a被淘汰");  
        cache.put("e", "value-e");// ④
        showCache(cache);// 显示缓存列表,a被淘汰  
      }
    
      /**
      * 遍历并打印缓存数据
      */
      protected <K, V> void showCache(Cache<K, V> cache){  
          showCache(cache, null);  
      }  
    
      protected <K, V> void showCache(Cache<K, V> cache, String no){  
      	Map<K, V> snapshot = cache.snapshot();  
      	if(MapUtils.isEmpty(snapshot)){  
            LOGGER.info("showTime: {} ==> {}", "null", "null");  
      		return;  
      	}  
        snapshot.forEach((K, V) -> {  
              if(StringUtils.isBlank(no)){  
                  LOGGER.info("showTime: {} ==> {}", K, V);  
      		  }else {  
                  LOGGER.info("showTime: [{}] {} ==> {}", no, K, V);  
      		  }  
        });  
      }
    

    代码解析:

    • 首先创建了个容量为3,且无过期时间的LRU策略的缓存数据结构
    • 代码①处赋值a,b,c
    • 代码②处a,b查询命中,会被依次放到队尾位置
    • 代码③处新增数据d,根据LRU淘汰策略,c会被清除以释放空间,然后d被放置到队尾位置,所以缓存中剩下a,b,d
    • 代码④处新增数据e,根据LRU淘汰策略,a会被清除以释放空间,然后e被放置到队尾位置,所以缓存中剩下b,d,e
    • Jodd的LRUCache之所以能实现根据最近使用情况排序的功能,底层是通过LinkedHashMap的accessOrder=true实现的,下一章笔者将带领大家解读源码,一览究竟

    输出:

      com.code.xxxxxxx.jodd.JoddTest.pruneCacheTest:36 --------c被淘汰--------
      com.code.xxxxxxx.jodd.JoddTest.lambda$showCache$0:23 -showTime: d ==> value-d
      com.code.xxxxxxx.jodd.JoddTest.lambda$showCache$0:23 -showTime: a ==> value-a
      com.code.xxxxxxx.jodd.JoddTest.lambda$showCache$0:23 -showTime: b ==> value-b
      com.code.xxxxxxx.jodd.JoddTest.pruneCacheTest:40 --------a被淘汰--------
      com.code.xxxxxxx.jodd.JoddTest.lambda$showCache$0:23 -showTime: d ==> value-d
      com.code.xxxxxxx.jodd.JoddTest.lambda$showCache$0:23 -showTime: e ==> value-e
      com.code.xxxxxxx.jodd.JoddTest.lambda$showCache$0:23 -showTime: b ==> value-b
    

3.2. LFU

  • 存、取和过期测试:

      此测试流程与FIFOCache一致,不再做演示
    
  • 淘汰策略测试

      @Test  
      public void pruneCacheTest(){  
          LFUCache<String, String> cache = new LFUCache<>(3);// ①  
    
      	  cache.put("a", "value-a");// ②  
      	  cache.put("b", "value-b");  
      	  cache.put("c", "value-c");  
    
      	  cache.get("a");// ③  
      	  cache.get("a");// 命中a两次  
      	  cache.get("c");// 命中c一次  
    
      	  cache.put("d", "value-d");// ④  
      	  showCache(cache);  
      }
    

    代码解析:

    • 代码①处,创建容量为3的LFUCache,且不设置默认过期时间
    • 代码②处,连续赋3个值a,b,c
    • 代码③处,连续命中a两次,和命中c一次
    • 代码④处,新增数据d到缓存,由于容量为3,所以触发淘汰策略,理论上会淘汰掉未被引用(命中次数最少)的b

    输出:

      com.code.xxxxxxx.jodd.JoddTest.lambda$showCache$0:23 -showTime: d ==> value-d
      com.code.xxxxxxx.jodd.JoddTest.lambda$showCache$0:23 -showTime: a ==> value-a
      com.code.xxxxxxx.jodd.JoddTest.lambda$showCache$0:23 -showTime: c ==> value-c
    

4. 总结

JoddCache是一款简单易用的本地缓存解决方案,简单易学好上手,与Jodd号称小而精巧的瑞士军刀相匹配,值得我们去学习和使用。

下一章笔者将带领大家阅读JoddCache,解开简单易用背后的面纱,敬请期待。