简介
缓存是我们平时使用最多的保存技术手段。不管是在spring内部使用map进行缓存对象,还是mysql的innoDB引擎中使用buffer进行一些数据缓存,亦或是我们在业务开发中为了减缓数据库的交互使用redis缓存。所以缓存技术和我们的日常开发息息相关,缓存可以粗略分为两大类,存储在redis的分布式缓存和存储在本地的JVM缓存。
本地缓存
本地缓存是最常见的使用内存的缓存,比如常用的HashMap、ArrayList都可以认为是缓存,只是生命周期与并发的问题,不经常作为全局的缓存使用。在此想要介绍的是Google Guava包中的Guava Cache。
Guava Cache
与ConcurrentHashMap对比
Guava Cache的数据结构类似于我们常用的ConcurrentHashMap,ConcurrentHashMap会一直保存元素,持续地添加元素会占用更多的内存从而影响程序性能,这是我们不愿意见到的。Guava Cache提供了淘汰的策略,能够有效控制缓存的内存大小。总而言之,Guava Cache有更多的特性,如果不需要这些特性,使用ConcurrentHashMap会有更好的内存效率。
与Memcached对比
Guava Cache不会将数据持久化到文件或者外部服务器,所以使用Guava Cache需要考虑使用的缓存大小不会超过内存总量,否则就需要考虑Memcached此类工具。
原理简介
如何进行缓存的获取与管理是至关重要的环节,Guava采取“获取缓存-如果没有-则计算”「get-if-absent-compute」的原子语义,这个场景就像是我们在查询数据数据库的时候,先查询redis-如果redis没有值-则从数据库中查询,数据库查询配合缓存就是一种具象的「get-if-absent-compute」场景。Guava推荐的实践方式也是定义CacheLoader自动加载load的方式实现缓存的计算和获取。
简单demo介绍
private LoadingCache<Long, String> loadingCacheInfo = CacheBuilder.newBuilder().maximumSize(10000)
.build(
new CacheLoader<Long, String>() {
@Override
public String load(Long aLong) {
if (aLong.equals(1L)) {
return "返回缓存生效";
}
return "返回缓存不生效";
}
}
);
声明一个loadingCacheInfo变量,类型需要声明进行类型检查(也可不声明具体类型,使用<Key, Graph>),但不推荐这种方式,在我们实际业务场景中我们是都可以预料到我们key和value的类型,从而能够明确确定类型。
CacheBuilder
是核心的一个构造类,可以构造我们的淘汰策略(3种淘汰策略,后续会着重介绍。CacheLoader通过重写load方法,来实现我们之前提到的计算方法,即在没有key对应的value时会计算得到值并放入缓存,下次查询的时候就会从缓存中取。
get
在业务代码中直接使用 loadingCacheInfo.get(1L) 的方式获取对应的缓存的value。注意Guava排斥结果值为null的结果,并会抛出异常,所以可以在load方法中包装结果值为Optional类进行使用。
淘汰策略
Guava提供了三种淘汰策略: 基于容量淘汰、定时淘汰、基于引用淘汰。
基于容量淘汰
规定缓存项的数目不超过固定值,使用CacheBuilder.maximumSize(long)的方式声明。
在缓存项的数目达到限定值之前,就可能进行回收操作。
定时淘汰
定时回收有两种方式。
-
expireAfterAccess(long, TimeUnit) — 缓存项在给定时间没有被读/写访问,则回收。
-
expireAfterWrite(long, TimeUnit) — 缓存项在给定时间内没有被写访问,则回收。
基于引用淘汰
通过使用弱引用的键、或弱引用的值、或软引用的值,Guava Cache可以把缓存设置为允许垃圾回收
主动淘汰
- 个别清除:Cache.invalidate(key)
- 批量清除:Cache.invalidateAll(keys)
- 清除所有缓存项:Cache.invalidateAll()
清除实践
使用CacheBuilder构建的缓存不会自动执行回收工作。会在写操作时做少量的维护工作,或者偶尔在读操作的时候执行回收工作。
这样做的原因在于:避免开启一个线程清理缓存,造成和用户操作的竞争共享锁。
如果使用缓存的场景是非高吞吐的,就可以创建自己的维护线程,固定时间间隔调用Cache.cleanUp()。