Guava Cache
Guava cache是一个应用内缓存(一般作为应用的本地缓存,redis作为集中式分布式缓存)。
一个缓存需要考虑的问题:
- 缓存读取失败如何加载数据
- 加载策略(同步还是异步)
- 缓存过期问题
- 统计缓存命中情况
- 缓存数据失效时设置监听
- 缓存满时替换策略(LRU、FIFO)
- ……
Guava简单的示例:
|
|
该示例中,仅仅将一个类型是字符串作为key,value为它的大写形式。运行的结果如下:
|
|
Guava的缓存是一个LoadingCache实例,通过CacheBuilder创建该实例,并传入一个CacheLoader,CacheLoader实例注明了在缓存读取失败时如何加载数据,开始时,缓存中没有任何数据,size为0,当取aaa的时候,触发了缓存加载数据,输出call…,虽然缓存的size变成了1。然后再取aaa时,因为缓存中已经有了该key对应的value,就没有触发加载。
需要注意一下getUnchecked方法和get方法的不同,前者不对可能的异常做检查,调用代码不需要显式的捕捉异常,而后者调用代码需要显式的捕获异常。
这是一个非常简单的示例,可以看到使用guava实现一个缓存非常简单,如果将创建CacheLoader实例和build LoadingCache的两行代码合并,使用仅一行代码就可以实现一个缓存,并且Guava的缓存是线程安全的,可以放心的在多线程的环境中使用。
复杂一点的例子:
|
|
结果如下:
|
|
创建了一个稍复杂的LoadingCache实例。各方法意义如下:
expireAfterWrite:写入缓存后的过期时间
maximumSize:缓存的最多存放元素个数
recordStats:对缓存命中情况进行统计
removalListener:设置缓存数据失效时监听器
Guava中很多地方都是这种fluent的方式.
在删除的监听器中打印线程的名字是为了显示该监听器是同步的还是异步的。可以看到删除监听是同步的,因为和主线程的名字是一样的,其实可以理解,因为我们并没有指定额外的线程池。删除监听器中可以看到删除的key、value、cause。主线程sleep 5s后,缓存中key为a的元素就过期了,可以看到监听器被调用,最后通过cache.stats()取得缓存命中的情况统计。可以看到命中1次,miss了4次(load了4次),事实上的确如此。
可以通过RemovalListeners.asynchronous方法就可以创建一个异步的listener对象。如下方式创建LoadingCache:
|
|
RemovalListeners.asynchronous方法接受两个参数,第一个参数是RemovalListener对象,第二个参数接收一个线程池,这样就可以异步的设置删除监听器了。运行可以看到主线程的线程名和监听器中的线程名是不同的。
上面创建缓存的方式是通过expireAfterWrite指定元素的过期时间,达到重新加载的。也就是说当过期后,这个元素就不存在了,再获取的时候就要通过load重新加载,当加载的时候,获取value的主线程必须同步的等缓存加载完获得数据后才能继续执行。这在一定程度上限制了访问速度。
如果数据量不大的情况下,就不必使用过期时间这种方式,而使用刷新,使用refreshAfterWrite指定刷新的时间间隔。看如下代码:
|
|
这是一个非常简单的refresh示例,如果使用refreshAfterWrite,需要实现CacheLoader的reload方法,如果不实现,他有一个默认的实现,就是本示例展示的代码,直接调用load方法。代码的运行结果如下:
|
|
本例中刷新的时间设置为3s,再第一次显式的调用cache.refresh("a")
的时候,可以看到reload方法被调用了。但是reload直接走默认的实现,调用了load方法,所以接着就输出了load key[a]
当主线程sleep 3s后,再取a的值时因为超过刷新间隔,又会调用reload方法。可以想象这里的reload肯定是以同步的方式进行的,因为我们并没有指定额外的线程池用来执行reload方法,也就是说当到达刷新时间间隔后,取value的主线程还是要等refresh结束,才能拿到数据后执行,这和刚才的expireAfterWrite方式差不多。Guava提供了异步刷新的方式,看代码:
|
|
本示例依然设置refresh时间为3s。重点是reload方法,先打印出reload执行所在的线程名,为了能清楚的看到主线程不需要等refresh完,这里sleep了1s。其他代码跟之前的差不多,运行结果如下:
|
|
当执行cache.refresh("a")
代码的时候,调用了reload方法,可以看到reload所在线程名是线程池中的。这句代码紧接着主线程sleep了3s,然后又去取a的值,按理说这时候a应该到达了刷新的时间间隔了,但是因为之前的reload方法执行就需要1s,所以对于a来说,还有1s的刷新时间剩余,所以这时取a的值,并不会触发reload。而紧接着取b的值就不同了,因为b没有被refresh过,这时候取b的值达到了刷新的时间间隔,所以会触发reload b。但是因为是异步的刷新,主线程根本不用等刷新完,所以立即输出了原来旧的值B,并立即输出了load c的结果,然后才看到 reload b的过程在继续进行,直到结束。
异步刷新,主线程永远不用等缓存的加载!现在在工作中所有使用Guava cahe的地方全部采用这种方式。
注意如下几点:
- refreshAfterWrite和expireAfterWrite的区别
- refreshAfterWrite只不过在刷新时间间隔到的时候,调用reload方法获取对于的key对于的value后替换当前内存中的key值。原来内存中的key对于的value是一直存在的。
- expireAfterWrite方式当达到过期时间后,内存中的对应的key-value就被删除了(应该是被动删除方式,其实还在内存中,获取key的瞬间被删除)。只能通过load方法重新加载key对于的value。
- refresh方式并不是达到时间间隔后就立即刷新,而是在get数据的时候,发现超过刷新时间间隔了才会刷新,是被动的方式。
- 只有缓存中存在的key,在到达刷新时间时,才会通过reload刷新,如果缓存中没有对应key的value,第一次永远是调用load加载数据。