简约而不简单的本地缓存解决方案
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 - 代码①处,创建容量为3的
4. 总结
JoddCache是一款简单易用的本地缓存解决方案,简单易学好上手,与Jodd号称小而精巧的瑞士军刀相匹配,值得我们去学习和使用。
下一章笔者将带领大家阅读JoddCache,解开简单易用背后的面纱,敬请期待。
浙公网安备 33010602011771号