点击上方“java全栈技术”关注,每天晚上学习一个java知识点
系统中,本地缓存适用于一些访问量较大、数据量较小、与业务无关的缓存。 为什么不使用分布式缓存呢? 分布式集群缓存的建立和维护成本较高,不适合紧急项目。 本地缓存访问速度快,使用方便,但缺点是无法保证数据更新的一致性,使用范围有限。 GuavaCache 是本地缓存的最佳选择,我们明天就会知道。 以下是本文的内容提纲:
通过guavacache的优势和使用场景来判断业务中是否适合使用这款缓存。 介绍常用技术,并给出示例,作为深入分析源码的参考。
番石榴简介
Guacache 是一个本地缓存。 具有以下优点:
常用技能
批量操作就是循环调用对应的方法,如:
GuavaCache中缓存的容器被定义为socket Cache的实现类。 该实现类是线程安全的,因此一般定义为单例。 以下是官方demo:
回收政策
常用的定时恢复,以下是三种基于时间的方法来清除或刷新缓存数据:
expireAfterAccess:当缓存项在指定时间段内没有被读写时,就会被回收。
expireAfterWrite:当缓存项在指定的时间内没有更新时,就会被回收。
freshAfterWrite:缓存项上次更新操作后多久刷新。
考虑到时效性,我们可以使用expireAfterWrite在每次更新后的指定时间使缓存失效,然后重新加载缓存。 Guacache会严格限制只有一次加载操作,这样会防止大量缓存失效请求渗透到前端,造成雪崩效应。
但通过分析源码,guavacache只锁定了一个加载操作,其他请求必须阻塞等待加载操作完成; 加载完成后,其他请求线程会一一获取锁,判断自己是否已被锁定。 加载完成后查看小程序缓存数据的网站,每个线程都要轮流经历“获取锁、获取值、释放锁”的过程,这会造成一定的性能损失。 这里,因为我们计划在本地缓存一秒,所以频繁的过期加载、锁等待等过程会造成性能上的较大损失。
为此,我们考虑使用refreshAfterWrite。 freshAfterWrite的特点是在刷新过程中,严格限制只有一次重载操作,而其他查询则先返回旧值,这样可以有效减少等待和锁争用,所以refreshAfterWrite会比expireAfterWrite表现更好。 而且它也有一个缺点,因为达到指定时间后,它不能严格保证所有查询都会得到新值。 了解过Guacache定时失效(或者说刷新)的朋友都知道,Guacache并没有使用额外的线程来执行定时的清除和加载功能,而是依赖于查询请求。 查询时,比较之前的更新时间,如果超过指定时间,则加载或刷新。 因此查看小程序缓存数据的网站,如果使用refreshAfterWrite,在吞吐量较低的情况下,如果长时间没有查询,发生的查询可能会得到一个盲值(这个盲值可能来自很久以前),这会导致问题。
可见refreshAfterWrite和expireAfterWrite这两种形式既有异同。
Guacache源码分析
示例代码:
首先了解一些主要的类和socket:
综上所述,guavacache的核心操作都是在LocalCache中实现的。
其他:
在看具体代码之前,我们先简单了解一下LocalCache的数据结构。
LocalCache的数据结构如下:
LocalCache的数据结构与ConcurrentHashMap非常相似。 两者都是由多个Segment组成,每个Segment相对独立,互不影响,因此可以支持并行操作。 每个段由一个表和多个队列组成。 缓存的数据存储在表中,表的类型为AtomicReferenceArray>,即链表,链表中的每个元素都是一个数组。 这两个队列分别是writeQueue和accessQueue,分别用于存储写入的数据和最近访问的数据。 当数据过期,需要刷新整体缓存时(见上例中最后一个cache.getIfPresent("key5")),就会遍历队列。 如果数据过期,则会从表中删除。 段中根据参考场景还有其他队列,这里不再讨论。
缓存生成器
CacheBuilder是一个缓存配置和建立入口,我们先看一些属性。 CacheBuilder的设置操作就是针对那些属性参数的。
CacheBuilder构建缓存有两种方式:
本地缓存
LocalCache是guavacache的核心类。 LocalCache的构造函数在里面已经分析过了,接下来看核心技巧。
对于get(key, loader)方法流程:
getAll(keys) 方法:
put(键,值)方法:
putAll(map) 在循环中调用 put 技巧。
在 putIfAbsent(key,value) 缓存中,只有当通配符对不存在时才会插入。
实践
Guavacache将数据源中的数据缓存在本地,那么如果我们想将远程数据源中的数据缓存在远程分布式缓存(如redis)中,我们该如何使用guavacache方法来封装呢?
可以模仿guava写一个简单的缓存,定义如下:
CacheBuilder类:配置缓存参数并构建缓存。 与前面提到的相同。
缓存socket:定义增、删、查的socket。
MyCache类:实现Cache套接字,put->store到DB,更新缓存; get->查询缓存,如果存在则返回; 如果没有,则查询DB,更新缓存,返回。
CacheLoader类:被MyCache调用,在get、getAll时提供单查询DB和批量查询DB。