有的小伙伴可能会有疑问,现在有很多成熟的开源库,比如JAVA项目的Guava cache
、Caffeine Cache
、Spring Cache
等(这些在我们的系列文章中,后面都会逐个介绍),它们都提供了相对完善、开箱即用的本地缓存能力,为什么这里还要去自己手写本地缓存呢?这不是重复造轮子吗?
是也?非也!在编码的进阶之路上,“会用”永远都只是让自己停留在入门级别。正所谓知其然更要知其所以然,通过一起探讨手写缓存的实现与设计关键点,来切身的体会蕴藏在缓存架构中的设计哲学。只有真正的掌握其原理,才能在使用中更好的去发挥其最大价值。
在一个项目系统中需要缓存数据的场景会非常多,而且需要缓存的数据类型也不尽相同。如果每个使用到缓存的地方,我们都单独的去实现一套缓存,那开发小伙伴们的工作量又要上升了,且后续各业务逻辑独立的缓存部分代码的维护也是一个可预见的头疼问题。
作为应对之法,我们的本地缓存必须往一个更高层级进行演进,使得项目中不同的缓存场景都可以通用 —— 也即将其抽象封装为一个通用的本地缓存框架
。既然定位为业务通用的本地缓存框架,那至少从规范或者能力层面,具备一些框架该有的样子:
泛型化设计,不同业务维度可以通用
标准化接口,满足大部分场景的使用诉求
轻量级集成,对业务逻辑不要有太强侵入性
多策略可选,允许选择不同实现策略甚至是缓存存储机制,打破众口难调的困局
下面,我们以上述几个点要求作为出发点,一起来勾勒一个符合上述诉求的本地缓存框架的模样。
在前一篇文章中,我们有介绍过项目中常见的缓存使用场景。基于提及的几种具体应用场景,我们可以归纳出业务对本地缓存的API接口层的一些共性诉求。如下表所示:
接口名称 | 含义说明 |
---|---|
get | 根据key查询对应的值 |
put | 将对应的记录添加到缓存中 |
remove | 将指定的缓存记录删除 |
containsKey | 判断缓存中是否有指定的值 |
clear | 清空缓存 |
getAll | 传入多个key,然后批量查询各个key对应的值,批量返回,提升调用方的使用效率 |
putAll | 一次性批量将多个键值对添加到缓存中,提升调用方的使用效率 |
putIfAbsent | 如果不存在的情况下则添加到缓存中,如果存在则不做操作 |
putIfPresent | 如果key已存在的情况下则去更新key对应的值,如果不存在则不做操作 |
为了满足一些场景对数据过期的支持,还需要提供或者重载一些接口用于设定过期时间:
接口名称 | 含义说明 |
---|---|
expireAfter | 用于指定某个记录的过期时间长度 |
put | 重载方法,增加过期时间的参数设定 |
putAll | 重载方法,增加过期时间的参数设定 |
基于上述提供的各个API方法,我们可以确定缓存的具体接口类定义:
- /**
- * 缓存容器接口
- *
- * @author 架构悟道
- * @since 2022/10/15
- */
- public interface ICache<K, V> {
- V get(K key);
- void put(K key, V value);
- void put(K key, V value, int timeIntvl, TimeUnit timeUnit);
- V remove(K key);
- boolean containsKey(K key);
- void clear();
- boolean containsValue(V value);
- Map<K, V> getAll(Set<K> keys);
- void putAll(Map<K, V> map);
- void putAll(M