1、ehcahce 什么时候用比较好;
2、问题:当有个消息的key不在guava里面的话,如果大量的消息过来,会同时请求数据库吗?还是只有一个请求数据库,其他的等待第一个把数据从DB加载到Guava中
回答:是的,其他的都会等待load,直到数据加载完毕;
2、recency queue 干嘛用的:
目前没看出来,但是应该是为了LRU队列也就是快速删除算法,因为recency queue的队列,如果读的话,会往recency queue和 access queue中写入数据,如果写的话,首先要清空recency queue队列,然后在recency queue中,然后再在access queue中写入队列;所以应该会为了快速删除过期数据准备的queue:
背景:
目前在网安部项目中,会接收到LBS消息 高峰期的QPS大约为5000,目前是直接通过LBS消息的订单ID查询 查询订单接口的数据,由于涉及到上游部署,或者网络抖动的问题,当上游积压时,订单经常会报警。因此考虑对缓存做一次调研。
在多线程高并发场景中往往是离不开cache的,需要根据不同的应用场景来需要选择不同的cache,比如分布式缓存如Redis、memcached,还有本地(进程内)缓存如ehcache、GuavaCache
缓存分为本地缓存和分布式缓存, 为什么要使用本地缓存呢?因为本地缓存比IO更高效,比分布式缓存更稳定。。
分布式缓存主要为redis或memcached之类的称为分布式缓存,
优点:
Redis 容量大,可以持久化,可以实现分布式的缓存,可以处理每秒百万级的并发,是专业的缓存服务,
redis可单独部署,多个项目之间可以共享,本地内存无法共享;
在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。
缺点:
需要保持redis或memcached服务的高可用,整个程序架构上较为复杂,硬件成本较高
本地缓存主要为Ecache和 guava Cache
区别:
- Ehcache支持持久化到本地磁盘,Guava不可以;
- Ehcache有现成的集群解决方案,Guava没有。不过个人感觉比较鸡肋,对JVM级别的缓存来讲太重了;
- Ehcache jar包庞大,Guava Cache只是Guava jar包中的工具之一,而且后者远远小于Ehcache;
- 两种缓存当缓存过期或者没有命中的时候都可以通过load接口重载数据,调用方式略有不同。两者的主要区别是Ehcache的缓存load的时候,允许用户返回null,而Guava Cache则不允许返回为null,因为Guava Cache是根据value的值是否为null来判断是否需要load,所以不允许返回为null,但是使用的时候可以使用空对象替换。不允许返回null是一个很好的考虑;
- Ehcache有内存占用大小统计,Guava Cache没有,需要自己开发;
- Ehcache在put缓存的时候,对K、V都做了包装,对GC有一定影响。
适用Ehcache的情况
- 需要使用持久化功能需要,缓存稳定,以免持久化的数据不准确影响结果。
- 有集群解决方案。
适用Guava cache的情况
Guava cache说简单点就是一个支持LRU的ConCurrentHashMap,它没有Ehcache那么多的各种特性,只是提供了增、删、改、查、刷新规则和时效规则设定等最基本的元素。做一个jar包中的一个功能之一,Guava cache极度简洁并能满足觉大部分人的要求。
Guava Cache 优点:
- 采用锁分段技术,锁粒度减小,加大并发。
- API优雅,简单可用,支持多种回收方式。
- 自带统计功能。
- Guava Cache的超时机制不是精确的;
缺点:
- 受内存大小限制不能存储太多数据
- 单JVM有效,非分布式缓存。多台服务可能有不同效果。
- 不能持久化本地缓存;
使用场景
愿意花费一部分内存来提高速度 -- 以空间换时间
期待有些关键字会被多次查询 -- 热点数据
不需要持久化
缓存中存放的数据总量不会超出内存容量。
总结
Ehcache有着全面的缓存特性,但是略重。Guava cache有最基本的缓存特性,很轻。
两种类型都是成熟的缓存框架,由于不需要保存到本地磁盘 考虑到Ehcahce 比较重,而Guava 比较轻量,考虑使用Guava
二、Guava Cache缓存数据结构
Guava工程包含了若干被Google的 Java项目广泛依赖 的核心库;Google Guava Cache是一种非常优秀本地缓存解决方案,提供了基于容量,时间和引用的缓存回收方式。
Guava Cache 其核心数据结构大体上和 ConcurrentHashMap 一致,具体细节上会有些区别。功能上,ConcurrentMap会一直保存所有添加的元素,直到显式地移除。相对地, Guava Cache 为了限制内存占用,通常都设定为自动回收元素。在某些场景下,尽管它不回收元素,也是很有用的,因为它会自动加载缓存。
Guava Cache与java1.7的ConcurrentMap很相似,但也不完全一样。最基本的区别是ConcurrentMap会一直保存所有添加的元素,直到显式地移除。相对地,Guava Cache为了限制内存占用,通常都设定为自动回收元素。
在这里就会涉及到segement的概念了,我们先把关系理清楚,首先看ConcurrentHashMap的图示,这样有助于我们理解:
Guava Cache中的核心类,重点了解。
2.1 LocalCache数据结构
ocalCache为Guava Cache的核心类,先看一个该类的数据结构: LocalCache的数据结构与ConcurrentHashMap很相似,都由多个segment组成,且各segment相对独立,互不影响,所以能支持并行操作。每个segment由一个table和若干队列组成。缓存数据存储在table中,其类型为AtomicReferenceArray。如下图所示 一个table 还有 5个queue;
对于每一个Segment 放大如下:包含了一个table 和5个队列;
LocalCache类似ConcurrentHashMap采用了分段策略,通过减小锁的粒度来提高并发,LocalCache中数据存储在Segment[]中,每个segment又包含5个队列和一个table
缓存核心类LocalCache,包含了Segment如下所示:
@GwtCompatible(emulated = true)
class LocalCache<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V> { final Segment<K, V>[] segments; @Nullable final CacheLoader<? super K, V> defaultLoader;
内部类Segment与jdk1.7及以前的ConcurrentHashMap非常相似,都继承于ReetrantLock,
static class Segment<K, V> extends ReentrantLock { @Weak final LocalCache<K, V> map; final ReferenceQueue<K> keyReferenceQueue;//key引用队列 final ReferenceQueue<V> valueReferenceQueue;//value引用队列 final Queue<ReferenceEntry<K, V>> recencyQueue;// LRU队列 @GuardedBy("this")
final Queue<ReferenceEntry<K, V>> writeQueue; // 写队列 @GuardedBy("this")
final Queue<ReferenceEntry<K, V>> accessQueue; //访问队列
Segment的构造函数:
Segment(
LocalCache<K, V> map,
int initialCapacity,
long maxSegmentWeight,
StatsCounter statsCounter) {
this.map = map;
this.maxSegmentWeight = maxSegmentWeight;
this.statsCounter = checkNotNull(statsCounter);
initTable(newEntryArray(initialCapacity)); keyReferenceQueue = map.usesKeyReferences() ? new ReferenceQueue<K>() : null; valueReferenceQueue = map.usesValueReferences() ? new ReferenceQueue<V>() : null; recencyQueue =
map.usesAccessQueue()
? new ConcurrentLinkedQueue<ReferenceEntry<K, V>>()
: LocalCache.<ReferenceEntry<K, V>>discardingQueue(); writeQueue =
map.usesWriteQueue()
? new WriteQueue<K, V>()
: LocalCache.<ReferenceEntry<K, V>>discardingQueue(); accessQueue =
map.usesAccessQueue()
? new AccessQueue<K, V>()
: LocalCache.<ReferenceEntry<K, V>>discardingQueue();
}
里面有一个table,五个queue,分别表示:读,写,最近使用,key,value 的queue;
里面涉及到引用的使用:
在 JDK1.2 之前这点设计的非常简单:一个对象的状态只有引用和没被引用两种区别。
因此 1.2 之后新增了四种状态用于更细粒度的划分引用关系:
强引用(Strong Reference):这种对象最为常见,比如 `A a = new A();`这就是典型的强引用;这样的强引用关系是不能被垃圾回收的。
软引用(Soft Reference):这样的引用表明一些有用但不是必要的对象,在将发生垃圾回收之前是需要将这样的对象再次回收。
弱引用(Weak Reference):这是一种比软引用还弱的引用关系,也是存放非必须的对象。当垃圾回收时,无论当前内存是否足够,这样的对象都会被回收。
虚引用(Phantom Reference):这是一种最弱的引用关系,甚至没法通过引用来获取对象,它唯一的作用就是在被回收时可以获得通知。
2.2 GuavaCache的 5个Queue的作用和 table的结构
基于引用的Entry,其实现类有弱引用Entry,强引用Entry等
ReferenceQueue<K> keyReferenceQueue;
已经被GC,需要内部清理的键引用队列。
ReferenceQueue<V> valueReferenceQueue;
已经被GC,需要内部清理的值引用队列。
Queue<ReferenceEntry<K, V>> recencyQueue;
记录升级可访问列表清单时的entries,当segment上达到临界值或发生写操作时该队列会被清空。
Queue<ReferenceEntry<K, V>> writeQueue;
按照写入时间进行排序的元素队列,写入一个元素时会把它加入到队列尾部。
Queue<ReferenceEntry<K, V>> accessQueue;
按照访问时间进行排序的元素队列,访问(包括写入)一个元素时会把它加入到队列尾部。
table的数据结构 类型为:AtomicReferenceArray
Segment继承于ReetrantLock,减小锁粒度,提高并发效率。
AtomicReferenceArray<ReferenceEntry<K, V>> table;
类似于HasmMap中的table一样,相当于entry的容器。
ReferenceEntry<K, V> referenceEntry;
(a) 这5个队列,实现了丰富的本地缓存方案。
这些队列,前2个是key、value引用队列用以加速GC回收,基于引用回收很好的利用了Java虚拟机的垃圾回收机制。
后3个队列记录用户的写记录、访问记录、高频访问顺序队列用以实现LRU算法。基于容量的方式内部实现采用LRU算法,
(b) AtomicReferenceArray是JUC包下的Doug Lea老李头设计的类:一组对象引用,其中元素支持原子性更新, 这个table是自定义的一种类数组的结构,每个元素都包含一个ReferenceEntry<k,v>链表,指向next entry。 采用了ReferenceEntry的方式,引用数据存储接口,默认强引用,对应的类图为:
问题1:为何LRU队列使用了 recencyQueue 队列 因为已经有了 accessQueue
keyReferenceQueue = map.usesKeyReferences() ? new ReferenceQueue<K>() : null; valueReferenceQueue = map.usesValueReferences() ? new ReferenceQueue<V>() : null; recencyQueue =
map.usesAccessQueue()
? new ConcurrentLinkedQueue<ReferenceEntry<K, V>>()
: LocalCache.<ReferenceEntry<K, V>>discardingQueue(); writeQueue =
map.usesWriteQueue()
? new WriteQueue<K, V>()
: LocalCache.<ReferenceEntry<K, V>>discardingQueue(); accessQueue =
map.usesAccessQueue()
? new AccessQueue<K, V>()
: LocalCache.<ReferenceEntry<K, V>>discardingQueue();
原因: 因为accessQueue是非线程安全的,get的时候使用并发工具ConcurrentLinkedQueue队列添加entry,而不用lock(),
ConcurrentLinkedQueue是一个基于链接节点的*非阻塞线程安全队列,其底层数据结构是使用单向链表实现,它采用先进先出的规则对节点进行排序,
recencyQueue 启用条件和accessQueue一样。每次访问操作都会将该entry加入到队列尾部,并更新accessTime。如果遇到写入操作,则将该队列内容排干,如果accessQueue队列中持有该这些 entry,然后将这些entry add到accessQueue队列。注意,因为accessQueue是非线程安全的,所以如果每次访问entry时就将该entry加入到accessQueue队列中,就会导致并发问题。所以这里每次访问先将entry临时加入到并发安全的ConcurrentLinkedQueue队列中,也就是recencyQueue中。在写入的时候通过加锁的方式,将recencyQueue中的数据添加到accessQueue队列中。 如此看来,recencyQueue是为 accessQueue服务的。以便高效的实现expireAfterAccess功能。 关于使用recencyQueue的好处:get的时候使用并发工具ConcurrentLinkedQueue队列添加entry,而不用lock(),一个是无阻赛锁一个是阻塞锁,
Cache类似于Map,它是存储键值对的集合,然而它和Map不同的是它还需要处理evict、expire、dynamic load等逻辑,需要一些额外信息来实现这些操作。在面向对象思想中,经常使用类对一些关联性比较强的数据做封装,同时把操作这些数据相关的操作放到该类中。因而Guava Cache使用ReferenceEntry接口来封装一个键值对,而用ValueReference来封装Value值。这里之所以用Reference命令,是因为Guava Cache要支持WeakReference Key和SoftReference、WeakReference value。
ValueReference
对于ValueReference,因为Guava Cache支持强引用的Value、SoftReference Value以及WeakReference Value,
因而它对应三个实现类:StrongValueReference、SoftValueReference、WeakValueReference。
为了支持动态加载机制,它还有一个LoadingValueReference,在需要动态加载一个key的值时,先把该值封装在LoadingValueReference中,以表达该key对应的值已经在加载了,如果其他线程也要查询该key对应的值,就能得到该引用,并且等待改值加载完成,从而保证该值只被加载一次(可以在evict以后重新加载)。在该只加载完成后,将LoadingValueReference替换成其他ValueReference类型。对新创建的LoadingValueReference,由于其内部oldValue的初始值是UNSET,它isActive为false,isLoading为false,因而此时的LoadingValueReference的isActive为false,但是isLoading为true。每个ValueReference都纪录了weight值,所谓weight从字面上理解是“该值的重量”,它由Weighter接口计算而得。weight在Guava Cache中由两个用途:1. 对weight值为0时,在计算因为size limit而evict是忽略该Entry(它可以通过其他机制evict);2. 如果设置了maximumWeight值,则当Cache中weight和超过了该值时,就会引起evict操作。但是目前还不知道这个设计的用途。最后,Guava Cache还定义了Stength枚举类型作为ValueReference的factory类,它有三个枚举值:Strong、Soft、Weak,这三个枚举值分别创建各自的ValueReference,并且根据传入的weight值是否为1而决定是否要创建Weight版本的ValueReference。以下是ValueReference的类
这里ValueReference之所以要有对ReferenceEntry的引用是因为在Value因为WeakReference、SoftReference被回收时,需要使用其key将对应的项从Segment的table中移除;copyFor()函数的存在是因为在expand(rehash)重新创建节点时,对WeakReference、SoftReference需要重新创建实例(个人感觉是为了保持对象状态不会相互影响,但是不确定是否还有其他原因),而对强引用来说,直接使用原来的值即可,这里很好的展示了对彼变化的封装思想;notifiyNewValue只用于LoadingValueReference,它的存在是为了对LoadingValueReference来说能更加及时的得到CacheLoader加载的值。
ReferenceEntry
ReferenceEntry是Guava Cache中对一个键值对节点的抽象。和ConcurrentHashMap一样,Guava Cache由多个Segment组成,而每个Segment包含一个ReferenceEntry数组,每个ReferenceEntry数组项都是一条ReferenceEntry链。并且一个ReferenceEntry包含key、hash、valueReference、next字段。除了在ReferenceEntry数组项中组成的链,在一个Segment中,所有ReferenceEntry还组成access链(accessQueue)和write链(writeQueue),这两条都是双向链表,分别通过previousAccess、nextAccess和previousWrite、nextWrite字段链接而成。在对每个节点的更新操作都会将该节点重新链到write链和access链末尾,并且更新其writeTime和accessTime字段,而没找到一个节点,都会将该节点重新链到access链末尾,并更新其accessTime字段。这两个双向链表的存在都是为了实现采用最近最少使用算法(LRU)的evict操作(expire、size limit引起的evict)。
Guava Cache中的ReferenceEntry可以是强引用类型的key,也可以WeakReference类型的key,为了减少内存使用量,还可以根据是否配置了expireAfterWrite、expireAfterAccess、maximumSize来决定是否需要write链和access链确定要创建的具体Reference:StrongEntry、StrongWriteEntry、StrongAccessEntry、StrongWriteAccessEntry等。创建不同类型的ReferenceEntry由其枚举工厂类EntryFactory来实现,它根据key的Strongth类型、是否使用accessQueue、是否使用writeQueue来决定不同的EntryFactry实例,并通过它创建相应的ReferenceEntry实例。ReferenceEntry类图如下:
WriteQueue和AccessQueue
为了实现最近最少使用算法,Guava Cache在Segment中添加了两条链:write链(writeQueue)和access链(accessQueue),这两条链都是一个双向链表,通过ReferenceEntry中的previousInWriteQueue、nextInWriteQueue和previousInAccessQueue、nextInAccessQueue链接而成,但是以Queue的形式表达。WriteQueue和AccessQueue都是自定义了offer、add(直接调用offer)、remove、poll等操作的逻辑,对于offer(add)操作,如果是新加的节点,则直接加入到该链的结尾,如果是已存在的节点,则将该节点链接的链尾;对remove操作,直接从该链中移除该节点;对poll操作,将头节点的下一个节点移除,并返回。
对于不需要维护WriteQueue和AccessQueue的配置(即没有expire time或size limit的evict策略)来说,我们可以使用DISCARDING_QUEUE以节省内存:
2.3 Guava Cache数据结构大类
先看一下google cache 核心类如下:
-
CacheBuilder:类,缓存构建器。构建缓存的入口,指定缓存配置参数并初始化本地缓存。
CacheBuilder在build方法中,会把前面设置的参数,全部传递给LocalCache,它自己实际不参与任何计算。采用构造器模式(Builder)使得初始化参数的方法值得借鉴,代码简洁易读。
CacheLoader:抽象类。用于从数据源加载数据,定义load、reload、loadAll等操作。
Cache:接口,定义get、put、invalidate等操作,这里只有缓存增删改的操作,没有数据加载的操作。
LoadingCache:接口,继承自Cache。定义get、getUnchecked、getAll等操作,这些操作都会从数据源load数据。
LocalCache:类。整个guava cache的核心类,包含了guava cache的数据结构以及基本的缓存的操作方法。
LocalManualCache:LocalCache内部静态类,实现Cache接口。其内部的增删改缓存操作全部调用成员变量localCache(LocalCache类型)的相应方法。
LocalLoadingCache:LocalCache内部静态类,继承自LocalManualCache类,实现LoadingCache接口。其所有操作也是调用成员变量localCache(LocalCache类型)的相应方法。
GuavaCache并不希望我们设置复杂的参数,而让我们采用建造者模式
创建Cache。GuavaCache分为两种Cache:Cache
,LoadingCache
。LoadingCache继承了Cache,他比Cache主要多了get和refresh方法。多这两个方法能干什么呢?
在第四节高级特性demo中,我们看到builder生成不带CacheLoader的Cache实例。在类结构图中其实是生成了LocalManualCache
类实例。而带CacheLoader的Cache实例生成的是LocalLoadingCache
。他可以定时刷新数据,因为获取数据的方法已经作为构造参数方法存入了Cache实例中。同样,在get时,不需要像LocalManualCache还需要传入一个Callable实例。
实际上,这两个Cache实现类都继承自LocalCache
,大部分实现都是父类做的。
LocalManualCache和LocalLoadingCache的选择
ManualCache
可以在get时动态设置获取数据的方法,而LoadingCache
可以定时刷新数据。如何取舍?我认为在缓存数据有很多种类的时候采用第一种cache。而数据单一,数据库数据会定时刷新时采用第二种cache。
先看下cache的类实现定义
class LocalCache<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V> {....}
我们看到了ConcurrentMap,所以我们知道了一点guava cache基于ConcurrentHashMap的基础上设计。所以ConcurrentHashMap的优点它也具备。既然实现了 ConcurrentMap那再看下guava cache中的Segment的实现是怎样?
我们看到guava cache 中的Segment本质是一个ReentrantLock。内部定义了table,wirteQueue,accessQueue定义属性。其中table是一个ReferenceEntry原子类数组,里面就存放了cache的内容。wirteQueue存放的是对table的写记录,accessQueue是访问记录。guava cache的expireAfterWrite,expireAfterAccess就是借助这个两个queue来实现的。
2.CacheBuilder构造器
private static final LoadingCache<String, String> cache = CacheBuilder.newBuilder()
.maximumSize(1000)
.refreshAfterWrite(1, TimeUnit.SECONDS)
//.expireAfterWrite(1, TimeUnit.SECONDS)
//.expireAfterAccess(1,TimeUnit.SECONDS)
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) throws Exception {
System.out.println(Thread.currentThread().getName() +"==load start=="+",时间=" + new Date());
// 模拟同步重载耗时2秒
Thread.sleep(2000);
String value = "load-" + new Random().nextInt(10);
System.out.println(
Thread.currentThread().getName() + "==load end==同步耗时2秒重载数据-key=" + key + ",value="+value+",时间=" + new Date());
return value;
} @Override
public ListenableFuture<String> reload(final String key, final String oldValue)
throws Exception {
System.out.println(
Thread.currentThread().getName() + "==reload ==异步重载-key=" + key + ",时间=" + new Date());
return service.submit(new Callable<String>() {
@Override
public String call() throws Exception {
/* 模拟异步重载耗时2秒 */
Thread.sleep(2000);
String value = "reload-" + new Random().nextInt(10);
System.out.println(Thread.currentThread().getName() + "==reload-callable-result="+value+ ",时间=" + new Date());
return value;
}
});
}
});
如上图所示:CacheBuilder参数设置完毕后最后调用build(CacheLoader )构造,参数是用户自定义的CacheLoader缓存加载器,复写一些方法(load,reload),返回LoadingCache接口(一种面向接口编程的思想,实际返回具体实现类)如下图:
public <K1 extends K, V1 extends V> LoadingCache<K1, V1> build(
CacheLoader<? super K1, V1> loader) {
checkWeightWithWeigher();
return new LocalCache.LocalLoadingCache<K1, V1>(this, loader);
}
实际是构造了一个LoadingCache接口的实现类:LocalCache的静态类LocalLoadingCache,本地加载缓存类。
LocalLoadingCache(
CacheBuilder<? super K, ? super V> builder, CacheLoader<? super K, V> loader) {
super(new LocalCache<K, V>(builder, checkNotNull(loader)));//LocalLoadingCache构造函数需要一个LocalCache作为参数
}
//构造LocalCache
LocalCache(
CacheBuilder<? super K, ? super V> builder, @Nullable CacheLoader<? super K, V> loader) {
concurrencyLevel = Math.min(builder.getConcurrencyLevel(), MAX_SEGMENTS);//默认并发水平是4 keyStrength = builder.getKeyStrength();//key的强引用
valueStrength = builder.getValueStrength(); keyEquivalence = builder.getKeyEquivalence();//key比较器
valueEquivalence = builder.getValueEquivalence(); maxWeight = builder.getMaximumWeight();
weigher = builder.getWeigher();
expireAfterAccessNanos = builder.getExpireAfterAccessNanos();//读写后有效期,超时重载
expireAfterWriteNanos = builder.getExpireAfterWriteNanos();//写后有效期,超时重载
refreshNanos = builder.getRefreshNanos(); removalListener = builder.getRemovalListener();//缓存触发失效 或者 GC回收软/弱引用,触发监听器
removalNotificationQueue =//移除通知队列
(removalListener == NullListener.INSTANCE)
? LocalCache.<RemovalNotification<K, V>>discardingQueue()
: new ConcurrentLinkedQueue<RemovalNotification<K, V>>(); ticker = builder.getTicker(recordsTime());
entryFactory = EntryFactory.getFactory(keyStrength, usesAccessEntries(), usesWriteEntries());
globalStatsCounter = builder.getStatsCounterSupplier().get();
defaultLoader = loader;//缓存加载器 int initialCapacity = Math.min(builder.getInitialCapacity(), MAXIMUM_CAPACITY);
if (evictsBySize() && !customWeigher()) {
initialCapacity = Math.min(initialCapacity, (int) maxWeight);
}
三、缓存的加载
Guava Cache为了限制内存占用,通常都设定为自动回收元素。在某些场景下,尽管LoadingCache 不回收元素,它也是很有用的,因为它会自动加载缓存。
guava cache 加载缓存主要有两种方式:
- cacheLoader
- callable callback
1、在创建的时候:cacheLoader
创建自己的CacheLoader通常只需要简单地实现V load(K key) throws Exception
方法.
cacheLoader方式实现实例:
LoadingCache<Key, Value> cache = CacheBuilder.newBuilder()
.build(
new CacheLoader<Key, Value>() {
public Value load(Key key) throws AnyException {
return createValue(key);
}
});
...
try {
return cache.get(key);
} catch (ExecutionException e) {
throw new OtherException(e.getCause());
}
从LoadingCache查询的正规方式是使用get(K)
方法。这个方法要么返回已经缓存的值,要么使用CacheLoader向缓存原子地加载新值(通过load(String key)
方法加载)。由于CacheLoader可能抛出异常,LoadingCache.get(K)
也声明抛出ExecutionException异常。如果你定义的CacheLoader没有声明任何检查型异常,则可以通过getUnchecked(K)
查找缓存;但必须注意,一旦CacheLoader声明了检查型异常,就不可以调用getUnchecked(K)
。
2、在get的时候:Callable
这种方式不需要在创建的时候指定load方法,但是需要在get的时候实现一个Callable匿名内部类。
Callable方式实现实例:
Cache<Key, Value> cache = CacheBuilder.newBuilder()
.build(); // look Ma, no CacheLoader
...
try {
// If the key wasn't in the "easy to compute" group, we need to
// do things the hard way.
cache.get(key, new Callable<Value>() {
@Override
public Value call() throws AnyException {
return doThingsTheHardWay(key);
}
});
} catch (ExecutionException e) {
throw new OtherException(e.getCause());
}
所有类型的Guava Cache,不管有没有自动加载功能,都支持get(K, Callable<V>)
方法。这个方法返回缓存中相应的值,或者用给定的Callable运算并把结果加入到缓存中。在整个加载方法完成前,缓存项相关的可观察状态都不会更改。这个方法简便地实现了模式"如果有缓存则返回;否则运算、缓存、然后返回"。
CacheBuilder.refreshAfterWrite(long, TimeUnit)
可以为缓存增加自动定时刷新功能。和expireAfterWrite
相反,refreshAfterWrite
通过定时刷新可以让缓存项保持可用,但请注意:缓存项只有在被检索时才会真正刷新(如果CacheLoader.refresh
实现为异步,那么检索不会被刷新拖慢)。因此,如果你在缓存上同时声明expireAfterWrite
和refreshAfterWrite
,缓存并不会因为刷新盲目地定时重置,如果缓存项没有被检索,那刷新就不会真的发生,缓存项在过期时间后也变得可以回收。
统计
guava cache为我们实现统计功能,这在其它缓存工具里面还是很少有的。
7) 统计缓存使用过程中命中率/异常率/未命中率等数据。
缓存命中率
:从缓存中获取到数据的次数/全部查询次数,命中率越高说明这个缓存的效率好。由于机器内存的限制,缓存一般只能占据有限的内存大小,缓存需要不定期的删除一部分数据,从而保证不会占据大量内存导致机器崩溃。
如何提高命中率呢?那就得从删除一部分数据着手了。目前有三种删除数据的方式,分别是:FIFO(先进先出)
、LFU(定期淘汰最少使用次数)
、LRU(淘汰最长时间未被使用)
。
guava cache 除了回收还提供一种刷新机制LoadingCache.refresh(K)
,他们的的区别在于,guava cache 在刷新时,其他线程可以继续获取它的旧值。这在某些情况是非常友好的。而回收的话就必须等新值加载完成以后才能继续读取。而且刷新是可以异步进行的。
如果刷新过程抛出异常,缓存将保留旧值,而异常会在记录到日志后被丢弃[swallowed]。
重载CacheLoader.reload(K, V)
可以扩展刷新时的行为,这个方法允许开发者在计算新值时使用旧的值
CacheBuilder.recordStats()
用来开启Guava Cache的统计功能。统计打开后,Cache.stats()
方法会返回CacheStats对象以提供如下统计信息:hitRate()
:缓存命中率;averageLoadPenalty()
:加载新值的平均时间,单位为纳秒;evictionCount()
:缓存项被回收的总数,不包括显式清除。
此外,还有其他很多统计信息。这些统计信息对于调整缓存设置是至关重要的,在性能要求高的应用中我们建议密切关注这些数据.
Cache初始化:
final static Cache<Integer, String> cache = CacheBuilder.newBuilder()
//设置cache的初始大小为10,要合理设置该值
.initialCapacity()
//设置并发数为5,即同一时间最多只能有5个线程往cache执行写入操作
.concurrencyLevel()
//设置cache中的数据在写入之后的存活时间为10秒
.expireAfterWrite(, TimeUnit.SECONDS)
//构建cache实例
.build();
常用接口:
/**
* 该接口的实现被认为是线程安全的,即可在多线程中调用
* 通过被定义单例使用
*/
public interface Cache<K, V> { /**
* 通过key获取缓存中的value,若不存在直接返回null
*/
V getIfPresent(Object key); /**
* 通过key获取缓存中的value,若不存在就通过valueLoader来加载该value
* 整个过程为 "if cached, return; otherwise create, cache and return"
* 注意valueLoader要么返回非null值,要么抛出异常,绝对不能返回null
*/
V get(K key, Callable<? extends V> valueLoader) throws ExecutionException; /**
* 添加缓存,若key存在,就覆盖旧值
*/
void put(K key, V value); /**
* 删除该key关联的缓存
*/
void invalidate(Object key); /**
* 删除所有缓存
*/
void invalidateAll(); /**
* 执行一些维护操作,包括清理缓存
*/
void cleanUp();
}
四、缓存回收机制
清理什么时候发生?
缓存清除的时间:
使用CacheBuilder构建的缓存不会"自动"执行清理和回收工作,也不会在某个缓存项过期后马上清理,也没有诸如此类的清理机制。GuavaCache的实现代码中没有启动任何线程,Cache中的所有维护操作,包括清除缓存、写入缓存等,都需要外部调用来实现 ,
相反,它会在写操作时顺带做少量的维护工作,或者偶尔在读操作时做——如果写操作实在太少的话。(问题2,如何实现的)
这样做的原因在于:如果要自动地持续清理缓存,就必须有一个线程,这个线程会和用户操作竞争共享锁。此外,某些环境下线程创建可能受限制,这样CacheBuilder就不可用了。
相反,我们把选择权交到你手里。如果你的缓存是高吞吐的,那就无需担心缓存的维护和清理等工作。如果你的 缓存只会偶尔有写操作,而你又不想清理工作阻碍了读操作,那么可以创建自己的维护线程,以固定的时间间隔调用Cache.cleanUp()。ScheduledExecutorService可以帮助你很好地实现这样的定时调度。
。回收时主要处理四个Queue:1. keyReferenceQueue;2. valueReferenceQueue;3. writeQueue;4. accessQueue。前两个queue是因为WeakReference、SoftReference被垃圾回收时加入的,清理时只需要遍历整个queue,将对应的项从LocalCache中移除即可,这里keyReferenceQueue存放ReferenceEntry,而valueReferenceQueue存放的是ValueReference。要从LocalCache中移除需要有key,因而ValueReference需要有对ReferenceEntry的引用。这里的移除通过LocalCache而不是Segment是因为在移除时因为expand(rehash)可能导致原来在某个Segment中的ReferenceEntry后来被移动到另一个Segment中了。
而对后面两个Queue,只需要检查是否配置了相应的expire时间,然后从头开始查找已经expire的Entry,将它们移除即可。
在put的时候,还会清理recencyQueue,即将recencyQueue中的Entry添加到accessEntry中.
guava cache基于ConcurrentHashMap的设计借鉴,在高并发场景支持线程安全,使用Reference引用命令,保证了GC的可回收到相应的数据,有效节省空间;同时write链和access链的设计,能更灵活、高效的实现多种类型的缓存清理策略,包括基于容量的清理、基于时间的清理、基于引用的清理等;
LRU缓存回收算法
LRU(Least Recently Used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。
1.新数据插入到链表头部;
2.每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
3.当链表满的时候,将链表尾部的数据丢弃。
Guava Cache中借助读写队列来实现LRU算法。
Guava Cache提供了四种基本的缓存回收方式:(a)基于容量回收、(b)定时回收 (c)基于引用回收 (d)显式清除
1. 基于容量回收(size-based eviction)
maximumSize(long):当缓存中的元素数量超过指定值时。
当缓存个数超过CacheBuilder.maximumSize(long)设置的值时,优先淘汰最近没有使用或者不常用的元素。同理
CacheBuilder.maximumWeight(long)也是一样逻辑。
如果要规定缓存项的数目不超过固定值,只需使用CacheBuilder.maximumSize(long)。缓存将尝试回收最近没有使用或总体上很少使用的缓存项。——警告:在缓存项的数目达到限定值之前,缓存就可能进行回收操作——通常来说,这种情况发生在缓存项的数目逼近限定值时。
b、定时回收(Timed Eviction)
2. 定时回收
CacheBuilder提供两种定时回收的方法:
(a)按照写入时间,最早写入的最先回收;
expireAfterAccess(long, TimeUnit):缓存项在给定时间内没有被读/写访问,则回收。请注意这种缓存的回收顺序和基于大小回收一样。
(b)按照访问时间,最早访问的最早回收。
expireAfterWrite(long, TimeUnit):缓存项在给定时间内没有被写访问(创建或覆盖),则回收。如果认为缓存数据总是在固定时候后变得陈旧不可用,这种回收方式是可取的。
清理发生时机
使用CacheBuilder构建的缓存不会”自动”执行清理和回收工作,也不会在某个缓存项过期后马上清理。相反,它会在写操作时顺带做少量的维护工作,或者偶尔在读操作时做——如果写操作实在太少的话。
3. 基于引用回收(Reference-based Eviction)
CacheBuilder.weakKeys():使用弱引用存储键。当键没有其它(强或软)引用时,缓存项可以被垃圾回收。
CacheBuilder.weakValues():使用弱引用存储值。当值没有其它(强或软)引用时,缓存项可以被垃圾回收。
CacheBuilder.softValues():使用软引用存储值。软引用只有在响应内存需要时,才按照全局最近最少使用的顺序回收。
在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Refernce)、虚引用(Phantom Reference)。四种引用强度依次减弱。这四种引用除了强引用(Strong Reference)之外,其它的引用所对应的对象来JVM进行GC时都是可以确保被回收的。所以通过使用弱引用的键、或弱引用的值、或软引用的值,Guava Cache可以把缓存设置为允许垃圾回收:
通过使用弱引用的键、或弱引用的值、或软引用的值,Guava Cache可以把缓存设置为允许垃圾回收:
CacheBuilder.weakKeys():使用弱引用存储键。当键没有其它(强或软)引用时,缓存项可以被垃圾回收。
因为垃圾回收仅依赖恒等式(==),使用弱引用键的缓存用==而不是equals比较键。
CacheBuilder.weakValues():使用弱引用存储值。当值没有其它(强或软)引用时,缓存项可以被垃圾回收。
因为垃圾回收仅依赖恒等式(==),使用弱引用值的缓存用==而不是equals比较值。
CacheBuilder.softValues():使用软引用存储值。软引用只有在响应内存需要时,才按照LRU(全局最近最少使用)的顺序回收。
考虑到使用软引用的性能影响,我们通常建议使用更有性能预测性的缓存大小限定(见上文,基于容量回收)。使用软引用值的缓存同样用==而不是equals比较值。
这样的好处就是当内存资源紧张时可以释放掉到缓存的内存。注意!CacheBuilder如果没有指明默认是强引用的,GC时如果没有元素到达指定的过期时间,内存是不能被回收的。
显式清除
任何时候,你都可以显式地清除缓存项,而不是等到它被回收:
个别清除:Cache.invalidate(key)
批量清除:Cache.invalidateAll(keys)
清除所有缓存项:Cache.invalidateAll()
这里说一个小技巧,由于guava cache是存在就取不存在就加载的机制,我们可以对缓存数据有修改的地方显示的把它清除掉,然后再有任务去取的时候就会去数据源重新加载,这样就可以最大程度上保证获取缓存的数据跟数据源是一致的。
回答:在缓存访问或者写的时候做了清理的操作:
不管是get,还是put每次都会遍历这五个queue;
1、Get 删除过期数据: 在getLiveValue中
1、再跟进去之前第 2189 行会发现先要判断 count 是否大于 0,这个 count 保存的是当前Segment中缓存元素的数量,并用 volatile 修饰保证了可见性。
2、根据方法名称可以看出是判断当前的 Entry 是否过期,该 entry 就是通过 key 查询到的。这里就很明显的看出是根据根据构建时指定的过期方式来判断当前 key 是否过期了。
如果过期就往下走,尝试进行过期删除(需要加锁,保证操作此Segment的线程安全)。
获取当前缓存的总数量
自减一(前面获取了锁,所以线程安全)
删除并将更新的总数赋值到 count。
而在查询时候顺带做了这些事情,但是如果该缓存迟迟没有访问也会存在数据不能被回收的情况,不过这对于一个高吞吐的应用来说也不是问题。
2、Put删除数据:
删除包含了两部分:(a)回收弱,软引用queue(b) 删除 access和write队列 中过期时间的数据
(a)回收keyReference 和 valueReference 队列 弱,软引用queue
(b)删除 access和write队列 中过期时间的数据
五、GuavaCache 的实现
GuavaCache的工作流程:获取数据->如果存在,返回数据->计算获取数据->存储返回
。由于特定的工作流程,使用者必须在创建Cache或者获取数据时指定不存在数据时应当怎么获取数据。GuavaCache采用LRU的工作原理,使用者必须指定缓存数据的大小,当超过缓存大小时,必定引发数据删除。GuavaCache还可以让用户指定缓存数据的过期时间,刷新时间等等很多有用的功能。
创建本地缓存
a.CacheLoader
/**
* CacheLoader 当检索不存在的时候,会自动的加载信息的!
*/
private static LoadingCache<String, String> loadingCache = CacheBuilder
.newBuilder()
.maximumSize()
.expireAfterWrite(, TimeUnit.SECONDS)
.concurrencyLevel()
.recordStats()
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) throws Exception {
String value = map.get(key);
log.info(" load value by key; key:{},value:{}", key, value);
return value;
}
}); public static String getValue(String key) {
try {
return loadingCache.get(key);
} catch (Exception e) {
log.warn(" get key error ", e);
return null;
}
}
b.Callable
private static Cache<String, String> cacheCallable = CacheBuilder
.newBuilder()
.maximumSize()
.expireAfterWrite(, TimeUnit.SECONDS)
.concurrencyLevel()
.recordStats()
.build();
/**
* Callable 如果有缓存则返回;否则运算、缓存、然后返回
*/
public static String getValue1(String key) {
try {
return cacheCallable.get(key, new Callable<String>() {
@Override
public String call() throws Exception {
String value = map.get(key);
log.info(" load value by key; key:{},value:{}", key, value);
return value;
}
});
} catch (Exception e) {
log.warn(" get key error ", e);
return null;
}
}
5.1、Guava Cache使用了 builder模式
使用构造器重载我们需要定义很多构造器,为了应对使用者不同的需求(有些可能只需要id,有些需要id和name,有些只需要name,......),理论上我们需要定义2^4 = 16个构造器,这只是4个参数,如果参数更多的话,那将是指数级增长,肯定是不合理的。要么你定义一个全部参数的构造器,使用者只能多传入一些不需要的属性值来匹配你的构造器。很明显这种构造器重载的方式对于多属性的情况是不完美的。 (问题3 当构造函数的属性比较多,时候可以使用)
六、CacheBuilder有3种失效重载模式
这里面有几个参数expireAfterWrite、expireAfterAccess、maximumSize其实这几个定义的都是过期策略。expireAfterWrite适用于一段时间cache可能会发先变化场景。expireAfterAccess是包括expireAfterWrite在内的,因为read和write操作都被定义的access操作。另外expireAfterAccess,expireAfterAccess都是受到maximumSize的限制。当缓存的数量超过了maximumSize时,guava cache会要据LRU算法淘汰掉最近没有写入或访问的数据。这
里的maximumSize指的是缓存的个数并不是缓存占据内存的大小。 如果想限制缓存占据内存的大小可以配置maximumWeight参数。
看代码:
CacheBuilder.newBuilder().weigher(new Weigher<String, Object>() { @Override
public int weigh(String key, Object value) {
return 0; //the value.size()
}
}).expireAfterWrite(10, TimeUnit.SECONDS).maximumWeight(500).build();
weigher返回每个cache value占据内存的大小,这个大小是由使用者自身定义的,并且put进内存时就已经确定后面就再不会发生变动。maximumWeight定义了所有cache value加起的weigher的总和不能超过的上限。
注意一点就是maximumWeight与maximumSize两者只能生效一个是不能同时使用的!
1.expireAfterWrite
当 创建 或 写之后的 固定 有效期到达时,数据会被自动从缓存中移除,
2.expireAfterAccess
指明每个数据实体:当 创建 或 写 或 读 之后的 固定值的有效期到达时,数据会被自动从缓存中移除。读写操作都会重置访问时间,但asMap方法不会。
3.refreshAfterWrite
指明每个数据实体:当 创建 或 写 之后的 固定值的有效期到达时,且新请求过来时,数据会被自动刷新(注意不是删除是异步刷新,不会阻塞读取,先返回旧值,异步重载到数据返回后复写新值)。
3.数据过期重载
数据过期不会自动重载,而是通过get操作时执行过期重载。具体就是上面追踪到了CacheBuilder构造的LocalLoadingCache,类图如下:
返回LocalCache.LocalLoadingCache后
就可以调用如下方法:
static class LocalLoadingCache<K, V> extends LocalManualCache<K, V>
implements LoadingCache<K, V> { LocalLoadingCache(
CacheBuilder<? super K, ? super V> builder, CacheLoader<? super K, V> loader) {
super(new LocalCache<K, V>(builder, checkNotNull(loader)));
} // LoadingCache methods @Override
public V get(K key) throws ExecutionException {
return localCache.getOrLoad(key);
} @Override
public V getUnchecked(K key) {
try {
return get(key);
} catch (ExecutionException e) {
throw new UncheckedExecutionException(e.getCause());
}
} @Override
public ImmutableMap<K, V> getAll(Iterable<? extends K> keys) throws ExecutionException {
return localCache.getAll(keys);
} @Override
public void refresh(K key) {
localCache.refresh(key);
} @Override
public final V apply(K key) {
return getUnchecked(key);
} // Serialization Support private static final long serialVersionUID = ; @Override
Object writeReplace() {
return new LoadingSerializationProxy<K, V>(localCache);
}
}
刷新:
V scheduleRefresh(
ReferenceEntry<K, V> entry,
K key,
int hash,
V oldValue,
long now,
CacheLoader<? super K, V> loader) {
if (map.refreshes()
&& (now - entry.getWriteTime() > map.refreshNanos)
&& !entry.getValueReference().isLoading()) {
V newValue = refresh(key, hash, loader, true);//重载数据
if (newValue != null) {//重载数据成功,直接返回
return newValue;
}
}//否则返回旧值
return oldValue;
}
刷新核心方法:
V refresh(K key, int hash, CacheLoader<? super K, V> loader, boolean checkTime) {
final LoadingValueReference<K, V> loadingValueReference =
insertLoadingValueReference(key, hash, checkTime);
if (loadingValueReference == null) {
return null;
}
//异步重载数据
ListenableFuture<V> result = loadAsync(key, hash, loadingValueReference, loader);
if (result.isDone()) {
try {
return Uninterruptibles.getUninterruptibly(result);
} catch (Throwable t) {
// don't let refresh exceptions propagate; error was already logged
}
}
return null;
} ListenableFuture<V> loadAsync(
final K key,
final int hash,
final LoadingValueReference<K, V> loadingValueReference,
CacheLoader<? super K, V> loader) {
final ListenableFuture<V> loadingFuture = loadingValueReference.loadFuture(key, loader);
loadingFuture.addListener(
new Runnable() {
@Override
public void run() {
try {
getAndRecordStats(key, hash, loadingValueReference, loadingFuture);
} catch (Throwable t) {
logger.log(Level.WARNING, "Exception thrown during refresh", t);
loadingValueReference.setException(t);
}
}
},
directExecutor());
return loadingFuture;
} public ListenableFuture<V> loadFuture(K key, CacheLoader<? super K, V> loader) {
try {
stopwatch.start();
V previousValue = oldValue.get();
if (previousValue == null) {
V newValue = loader.load(key);
return set(newValue) ? futureValue : Futures.immediateFuture(newValue);
}
ListenableFuture<V> newValue = loader.reload(key, previousValue);
if (newValue == null) {
return Futures.immediateFuture(null);
}
// To avoid a race, make sure the refreshed value is set into loadingValueReference
// *before* returning newValue from the cache query.
return transform(
newValue,
new com.google.common.base.Function<V, V>() {
@Override
public V apply(V newValue) {
LoadingValueReference.this.set(newValue);
return newValue;
}
},
directExecutor());
} catch (Throwable t) {
ListenableFuture<V> result = setException(t) ? futureValue : fullyFailedFuture(t);
if (t instanceof InterruptedException) {
Thread.currentThread().interrupt();
}
return result;
}
}
如上图,最终刷新调用的是CacheBuilder中预先设置好的CacheLoader接口实现类的reload方法实现的异步刷新。
返回get主方法,如果当前segment中找不到key对应的实体,同步阻塞重载数据:
V lockedGetOrLoad(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException {
ReferenceEntry<K, V> e;
ValueReference<K, V> valueReference = null;
LoadingValueReference<K, V> loadingValueReference = null;
boolean createNewEntry = true; lock();
try {
// re-read ticker once inside the lock
long now = map.ticker.read();
preWriteCleanup(now); int newCount = this.count - ;
AtomicReferenceArray<ReferenceEntry<K, V>> table = this.table;
int index = hash & (table.length() - );
ReferenceEntry<K, V> first = table.get(index); for (e = first; e != null; e = e.getNext()) {
K entryKey = e.getKey();
if (e.getHash() == hash
&& entryKey != null
&& map.keyEquivalence.equivalent(key, entryKey)) {
valueReference = e.getValueReference();
if (valueReference.isLoading()) {//如果正在重载,那么不需要重新再新建实体对象
createNewEntry = false;
} else {
V value = valueReference.get();
if (value == null) {//如果被GC回收,添加进移除队列,等待remove监听器执行
enqueueNotification(
entryKey, hash, value, valueReference.getWeight(), RemovalCause.COLLECTED);
} else if (map.isExpired(e, now)) {//如果缓存过期,添加进移除队列,等待remove监听器执行
// This is a duplicate check, as preWriteCleanup already purged expired
// entries, but let's accomodate an incorrect expiration queue.
enqueueNotification(
entryKey, hash, value, valueReference.getWeight(), RemovalCause.EXPIRED);
} else {//不在重载,直接返回value
recordLockedRead(e, now);
statsCounter.recordHits();
// we were concurrent with loading; don't consider refresh
return value;
} // immediately reuse invalid entries
writeQueue.remove(e);
accessQueue.remove(e);
this.count = newCount; // write-volatile
}
break;
}
}
//需要新建实体对象
if (createNewEntry) {
loadingValueReference = new LoadingValueReference<K, V>(); if (e == null) {
e = newEntry(key, hash, first);
e.setValueReference(loadingValueReference);
table.set(index, e);//把新的ReferenceEntry<K, V>引用实体对象添加进table
} else {
e.setValueReference(loadingValueReference);
}
}
} finally {
unlock();
postWriteCleanup();
}
//需要新建实体对象
if (createNewEntry) {
try {
// Synchronizes on the entry to allow failing fast when a recursive load is
// detected. This may be circumvented when an entry is copied, but will fail fast most
// of the time.
synchronized (e) {//同步重载数据
return loadSync(key, hash, loadingValueReference, loader);
}
} finally {
statsCounter.recordMisses();
}
} else {
// 重载中,说明实体已存在,等待重载完毕
return waitForLoadingValue(e, key, valueReference);
}
}
七、GuavaCache使用
首先定义一个需要存储的Bean,对象Man:
/**
* @author jiangmitiao
* @version V1.0
* @Title: 标题
* @Description: Bean
* @date 2016/10/27 10:01
*/
public class Man {
//身份证号
private String id;
//姓名
private String name; public String getId() {
return id;
} public void setId(String id) {
this.id = id;
} public String getName() {
return name;
} public void setName(String name) {
this.name = name;
} @Override
public String toString() {
return "Man{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
'}';
}
}
接下来我们写一个Demo:
import com.google.common.cache.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.*; /**
* @author jiangmitiao
* @version V1.0
* @Description: Demo
* @date 2016/10/27 10:00
*/
public class GuavaCachDemo {
private LoadingCache<String,Man> loadingCache; //loadingCache
public void InitLoadingCache() {
//指定一个如果数据不存在获取数据的方法
CacheLoader<String, Man> cacheLoader = new CacheLoader<String, Man>() {
@Override
public Man load(String key) throws Exception {
//模拟mysql操作
Logger logger = LoggerFactory.getLogger("LoadingCache");
logger.info("LoadingCache测试 从mysql加载缓存ing...(2s)");
Thread.sleep();
logger.info("LoadingCache测试 从mysql加载缓存成功");
Man tmpman = new Man();
tmpman.setId(key);
tmpman.setName("其他人");
if (key.equals("")) {
tmpman.setName("张三");
return tmpman;
}
if (key.equals("")) {
tmpman.setName("李四");
return tmpman;
}
return tmpman;
}
};
//缓存数量为1,为了展示缓存删除效果
loadingCache = CacheBuilder.newBuilder().maximumSize().build(cacheLoader);
}
//获取数据,如果不存在返回null
public Man getIfPresentloadingCache(String key){
return loadingCache.getIfPresent(key);
}
//获取数据,如果数据不存在则通过cacheLoader获取数据,缓存并返回
public Man getCacheKeyloadingCache(String key){
try {
return loadingCache.get(key);
} catch (ExecutionException e) {
e.printStackTrace();
}
return null;
}
//直接向缓存put数据
public void putloadingCache(String key,Man value){
Logger logger = LoggerFactory.getLogger("LoadingCache");
logger.info("put key :{} value : {}",key,value.getName());
loadingCache.put(key,value);
}
}
接下来,我们写一些测试方法,检测一下
public class Test {
public static void main(String[] args){
GuavaCachDemo cachDemo = new GuavaCachDemo()
System.out.println("使用loadingCache");
cachDemo.InitLoadingCache(); System.out.println("使用loadingCache get方法 第一次加载");
Man man = cachDemo.getCacheKeyloadingCache("");
System.out.println(man); System.out.println("\n使用loadingCache getIfPresent方法 第一次加载");
man = cachDemo.getIfPresentloadingCache("");
System.out.println(man); System.out.println("\n使用loadingCache get方法 第一次加载");
man = cachDemo.getCacheKeyloadingCache("");
System.out.println(man); System.out.println("\n使用loadingCache get方法 已加载过");
man = cachDemo.getCacheKeyloadingCache("");
System.out.println(man); System.out.println("\n使用loadingCache get方法 已加载过,但是已经被剔除掉,验证重新加载");
man = cachDemo.getCacheKeyloadingCache("");
System.out.println(man); System.out.println("\n使用loadingCache getIfPresent方法 已加载过");
man = cachDemo.getIfPresentloadingCache("");
System.out.println(man); System.out.println("\n使用loadingCache put方法 再次get");
Man newMan = new Man();
newMan.setId("");
newMan.setName("额外添加");
cachDemo.putloadingCache("",newMan);
man = cachDemo.getCacheKeyloadingCache("");
System.out.println(man);
}
}
guava cache使用简介
guava cache 是利用CacheBuilder类用builder模式构造出两种不同的cache加载方式CacheLoader,Callable,共同逻辑都是根据key是加载value。不同的地方在于CacheLoader的定义比较宽泛,是针对整个cache定义的,可以认为是统一的根据key值load value的方法,而Callable的方式较为灵活,允许你在get的时候指定load方法。看以下代码
Cache<String,Object> cache = CacheBuilder.newBuilder()
.expireAfterWrite(, TimeUnit.SECONDS).maximumSize().build(); cache.get("key", new Callable<Object>() { //Callable 加载
@Override
public Object call() throws Exception {
return "value";
}
}); LoadingCache<String, Object> loadingCache = CacheBuilder.newBuilder()
.expireAfterAccess(, TimeUnit.SECONDS).maximumSize()
.build(new CacheLoader<String, Object>() {
@Override
public Object load(String key) throws Exception {
return "value";
}
});
加载
CacheLoader
如果有合理的默认方法来加载或计算与键关联的值。
LoadingCache是附带CacheLoader构建而成的缓存实现。创建自己的CacheLoader通常只需要简单地实现V load(K key) throws Exception方法。
从LoadingCache查询的正规方式是使用get(K)方法。这个方法要么返回已经缓存的值,要么使用CacheLoader向缓存原子地加载新值。由于CacheLoader可能抛出异常,LoadingCache.get(K)也声明为抛出ExecutionException异常。
Callable
如果没有合理的默认方法来加载或计算与键关联的值,或者想要覆盖默认的加载运算,同时保留“获取缓存-如果没有-则计算”[get-if-absent-compute]的原子语义。
所有类型的Guava Cache,不管有没有自动加载功能,都支持get(K, Callable<V>)方法。这个方法返回缓存中相应的值,或者用给定的Callable运算并把结果加入到缓存中。在整个加载方法完成前,缓存项相关的可观察状态都不会更改。这个方法简便地实现了模式"如果有缓存则返回;否则运算、缓存、然后返回"。
但自动加载是首选的,因为它可以更容易地推断所有缓存内容的一致性。
使用cache.put(key, value)方法可以直接向缓存中插入值,这会直接覆盖掉给定键之前映射的值。使用Cache.asMap()视图提供的任何方法也能修改缓存。但请注意,asMap视图的任何方法都不能保证缓存项被原子地加载到缓存中。进一步说,asMap视图的原子运算在Guava Cache的原子加载范畴之外,所以相比于Cache.asMap().putIfAbsent(K,V),Cache.get(K, Callable<V>) 应该总是优先使用。
统计
CacheBuilder.recordStats():用来开启Guava Cache的统计功能。统计打开后,Cache.stats()方法会返回CacheS tats 对象以提供如下统计信息:
hitRate():缓存命中率;
averageLoadPenalty():加载新值的平均时间,单位为纳秒;
evictionCount():缓存项被回收的总数,不包括显式清除。
此外,还有其他很多统计信息。这些统计信息对于调整缓存设置是至关重要的,在性能要求高的应用中我们建议密切关注这些数据。
Demo3:
public class GuavaCacheDemo3 { static Cache<String, Object> testCache = CacheBuilder.newBuilder()
.weakValues()
.recordStats()
.build(); public static void main(String[] args){
Object obj1 = new Object(); testCache.put("",obj1); obj1 = new String(""); System.gc(); System.out.println(testCache.getIfPresent("")); System.out.println(testCache.stats()); }
}
运行结果
3、Guava Cache 主要的类图:
CacheBuilder
缓存构建器。构建缓存的入口,指定缓存配置参数并初始化本地缓存。
主要采用builder的模式,CacheBuilder的每一个方法都返回这个CacheBuilder知道build方法的调用。
注意build方法有重载,带有参数的为构建一个具有数据加载功能的缓存,不带参数的构建一个没有数据加载功能的缓存。
LocalManualCache
作为LocalCache的一个内部类,在构造方法里面会把LocalCache类型的变量传入,并且调用方法时都直接或者间接调用LocalCache里面的方法。
LocalLoadingCache
可以看到该类继承了LocalManualCache并实现接口LoadingCache。
覆盖了get,getUnchecked等方法。
2、localManualCache如何实现的
上一篇文章讲了LocalCache是如何通过Builder构建出来的,这篇文章重点是讲localCache的原理,首先通过类图理清涉及到相关类的关系,如下图我们可以看到,guava Cache的核心就是LocalCache,LocalCache实现了ConcurrentMap,并继承了抽象的map,关于ConcurrentMap的实现可以看这篇文章,讲的是并发hashmap的实现,对理解这篇文章有帮助。对于构造LocalCache最直接的两个相关类是LocalManualCache和LocalLoadingCache。
LocalManualCache和LocalLoadingCache
那么这个LoadingCache到底是什么作用呢,其实就是LocalCache对外暴露了实现的方法,所有暴露的方法都是实现了这个接口,LocalLoadingCache就是实现了这个接口,
特殊的是它是LocalCache的内部静态类,这个LocalLoadingCache内部静态类只是降低了LocalCache的复杂度,它是完全独立于LocalCache的。下边是我们使用的方法都是LocalCache接口的方法
说完了LocalLoadingCache我们看下LocalManualCache的作用,LocalManualCache是LocalLoadingCache的父类,LocalManualCache实现了Cache,所以LocalManualCache具有了所有Cache的方法,LocalLoadingCache是继承自LocalManualCache同样获得了Cache的所有方法,但是LocalLoadingCache可以选择的重载LocalManualCache中的方法,这样的设计有很大的灵活性;guava cache的内部实现用的LocalCache,但是对外暴露的是LocalLoadingCache,很好隐藏了细节,总结来说
1、LocalManualCache实现了Cache,具有了所有cache方法。
2、LocalLoadingCache实现了LoadingCache,具有了所有LoadingCache方法。
3、LocalLoadingCache继承了LocalManualCache,那么对外暴露的LocalLoadingCache的方法既有自身需要的,又有cache应该具有的。
4、通过LocalLoadingCache和LocalManualCache的父子关系实现了LocalCache的细节。
Guava Cache到底是如何进行缓存的
我们现在通过类图和源码的各种继承关系理清了这两个LocalLoadingCache和LocalManualCache的重要关系,下边我们再继续深入,通过我们常用的get方法进入:
/**
* LocalLoadingCache中的get方法,localCache是父类LocalManualCache的
*/
@Override
public V get(K key) throws ExecutionException {
return localCache.getOrLoad(key);
} /**
* 这个get和getOrLoad是AccessQueue中的方法,AccessQueue是何方神圣呢,我们通过类图梳理一下他们的关系
*/
V get(K key, CacheLoader<? super K, V> loader) throws ExecutionException {
int hash = hash(checkNotNull(key));
return segmentFor(hash).get(key, hash, loader);
} V getOrLoad(K key) throws ExecutionException {
return get(key, defaultLoader);
}
很明显这是队列,这两个队列的作用如下
WriteQueue:按照写入时间进行排序的元素队列,写入一个元素时会把它加入到队列尾部。
AccessQueue:按照访问时间进行排序的元素队列,访问(包括写入)一个元素时会把它加入到队列尾部。
我们来看下ReferenceEntry接口的代码,具备了一个Entry所需要的元素
interface ReferenceEntry<K, V> {
/**
* Returns the value reference from this entry.
*/
ValueReference<K, V> getValueReference(); /**
* Sets the value reference for this entry.
*/
void setValueReference(ValueReference<K, V> valueReference); /**
* Returns the next entry in the chain.
*/
@Nullable
ReferenceEntry<K, V> getNext(); /**
* Returns the entry's hash.
*/
int getHash(); /**
* Returns the key for this entry.
*/
@Nullable
K getKey(); /*
* Used by entries that use access order. Access entries are maintained in a doubly-linked list.
* New entries are added at the tail of the list at write time; stale entries are expired from
* the head of the list.
*/ /**
* Returns the time that this entry was last accessed, in ns.
*/
long getAccessTime();
5、Guava Cache 如何加载数据
不管性能,还是可用性来说, Guava Cache 绝对是本地缓存类库中首要推荐的工具类。其提供的 Builder模式 的CacheBuilder生成器来创建缓存的方式,十分方便,并且各个缓存参数的配置设置,类似于函数式编程的写法,也特别棒。
在官方文档中,提到三种方式加载 <key,value> 到缓存中。分别是:
LoadingCache 在构建缓存的时候,使用build方法内部调用 CacheLoader 方法加载数据;
在使用get方法的时候,如果缓存不存在该key或者key过期等,则调用 get(K, Callable<V>) 方式加载数据;
使用粗暴直接的方式,直接想缓存中put数据。
需要说明的是,如果不能通过key快速计算出value时,则还是不要在初始化的时候直接调用 CacheLoader 加载数据到缓存中。
加载
在使用缓存前,首先问自己一个问题:有没有合理的默认方法来加载或计算与键关联的值?如果有的话,你应当使用CacheLoader。如果没有,或者你想要覆盖默认的加载运算,同时保留”获取缓存-如果没有-则计算”[get-if-absent-compute]的原子语义,你应该在调用get时传入一个Callable实例。缓存元素也可以通过Cache.put方法直接插入,但自动加载是首选的,因为它可以更容易地推断所有缓存内容的一致性。自动加载就是createCacheLoader中的,当cache.get(key)不存在的时候,会主动的去加载值的信息并放进缓存中去。
Guava Cache有以下两种创建方式:
创建 CacheLoader
LoadingCache是附带CacheLoader构建而成的缓存实现。创建自己的CacheLoader通常只需要简单地实现V load(K key) throws Exception方法。例如,你可以用下面的代码构建LoadingCache:
CacheLoader: 当检索不存在的时候,会自动的加载信息的!
public static com.google.common.cache.CacheLoader<String, Employee> createCacheLoader() {
return new com.google.common.cache.CacheLoader<String, Employee>() {
@Override
public Employee load(String key) throws Exception {
log.info("加载创建key:" + key);
return new Employee(key, key + "dept", key + "id");
}
};
}
LoadingCache<String, Employee> cache = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterAccess(30L, TimeUnit.MILLISECONDS)
.build(createCacheLoader());
创建 Callable
所有类型的Guava Cache,不管有没有自动加载功能,都支持get(K, Callable)方法。这个方法返回缓存中相应的值,或者用给定的Callable运算并把结果加入到缓存中。在整个加载方法完成前,缓存项相关的可观察状态都不会更改。这个方法简便地实现了模式”如果有缓存则返回;否则运算、缓存、然后返回”。
Cache<Key, Value> cache = CacheBuilder.newBuilder()
.maximumSize(1000)
.build(); // look Ma, no CacheLoader
...
try {
// If the key wasn't in the "easy to compute" group, we need to
// do things the hard way.
cache.get(key, new Callable<Value>() {
@Override
public Value call() throws AnyException {
return doThingsTheHardWay(key);
}
});
} catch (ExecutionException e) {
throw new OtherException(e.getCause());
}
7、刷新
刷新和回收不太一样。正如LoadingCache.refresh(K)所声明,刷新表示为键加载新值,这个过程可以是异步的。在刷新操作进行时,缓存仍然可以向其他线程返回旧值,而不像回收操作,读缓存的线程必须等待新值加载完成。
CacheBuilder.refreshAfterWrite(long, TimeUnit)可以为缓存增加自动定时刷新功能。和expireAfterWrite相反,refreshAfterWrite通过定时刷新可以让缓存项保持可用,但请注意:缓存项只有在被检索时才会真正刷新(如果CacheLoader.refresh实现为异步,那么检索不会被刷新拖慢)。因此,如果你在缓存上同时声明expireAfterWrite和refreshAfterWrite,缓存并不会因为刷新盲目地定时重置,如果缓存项没有被检索,那刷新就不会真的发生,缓存项在过期时间后也变得可以回收。
2.1 Guava Cache使用示例
import java.util.Date;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.cache.RemovalListener;
import com.google.common.cache.RemovalNotification; /**
* @author tao.ke Date: 14-12-20 Time: 下午1:55
* @version \$Id$
*/
public class CacheSample { private static final Logger logger = LoggerFactory.getLogger(CacheSample.class); // Callable形式的Cache
private static final Cache<String, String> CALLABLE_CACHE = CacheBuilder.newBuilder()
.expireAfterWrite(, TimeUnit.SECONDS).maximumSize().recordStats()
.removalListener(new RemovalListener<Object, Object>() {
@Override
public void onRemoval(RemovalNotification<Object, Object> notification) {
logger.info("Remove a map entry which key is {},value is {},cause is {}.", notification.getKey(),
notification.getValue(), notification.getCause().name());
}
}).build(); // CacheLoader形式的Cache
private static final LoadingCache<String, String> LOADER_CACHE = CacheBuilder.newBuilder()
.expireAfterAccess(, TimeUnit.SECONDS).maximumSize().recordStats().build(new CacheLoader<String, String>() {
@Override
public String load(String key) throws Exception {
return key + new Date();
}
}); public static void main(String[] args) throws Exception { int times = ;
while (times-- > ) { Thread.sleep(); String valueCallable = CALLABLE_CACHE.get("key", new Callable<String>() {
@Override
public String call() throws Exception {
return "key" + new Date();
}
}); logger.info("Callable Cache ----->>>>> key is {},value is {}", "key", valueCallable);
logger.info("Callable Cache ----->>>>> stat miss:{},stat hit:{}",CALLABLE_CACHE.stats().missRate(),CALLABLE_CACHE.stats().hitRate()); String valueLoader = LOADER_CACHE.get("key"); logger.info("Loader Cache ----->>>>> key is {},value is {}", "key", valueLoader);
logger.info("Loader Cache ----->>>>> stat miss:{},stat hit:{}",LOADER_CACHE.stats().missRate(),LOADER_CACHE.stats().hitRate()); } }
}
上述代码,简单的介绍了 Guava Cache 的使用,给了两种加载构建Cache的方式。在 Guava Cache 对外提供的方法中, recordStats 和 removalListener 是两个很有趣的接口,可以很好的帮我们完成统计功能和Entry移除引起的监听触发功能。
此外,虽然在 Guava Cache 对外方法接口中提供了丰富的特性,但是如果我们在实际的代码中不是很有需要的话,建议不要设置这些属性,因为会额外占用内存并且会多一些处理计算工作,不值得。
Guava Cache 分析前置知识
Guava Cache 就是借鉴Java的 ConcurrentHashMap 的思想来实现一个本地缓存,但是它内部代码实现的时候,还是有很多非常精彩的设计实现,并且如果对 ConcurrentHashMap 内部具体实现不是很清楚的话,通过阅读 Cache 的实现,对 ConcurrentHashMap 的实现基本上会有个全面的了解。
3.1 Builder模式
设计模式之 Builder模式 在Guava中很多地方得到的使用。 Builder模式 是将一个复杂对象的构造与其对应配置属性表示的分离,也就是可以使用基本相同的构造过程去创建不同的具体对象。
Builder模式典型的结构图如:
Builder:为创建一个Product对象的各个部件制定抽象接口;
ConcreteBuilder:具体的建造者,它负责真正的生产;
Director:导演, 建造的执行者,它负责发布命令;
Product:最终消费的产品
Builder模式 的关键是其中的Director对象并不直接返回对象,而是通过(BuildPartA,BuildPartB,BuildPartC)来一步步进行对象的创建。当然这里Director可以提供一个默认的返回对象的接口(即返回通用的复杂对象的创建,即不指定或者特定唯一指定BuildPart中的参数)。
Tips:在 Effective Java 第二版中, Josh Bloch 在第二章中就提到使用Builder模式处理需要很多参数的构造函数。他不仅展示了Builder的使用,也描述了相这种方法相对使用带很多参数的构造函数带来的好处。
下面给出一个使用Builder模式来构造对象,这种方式优点和不足(代码量增加)非常明显。
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle; /**
* @author tao.ke Date: 14-12-22 Time: 下午8:57
* @version \$Id$
*/
public class BuilderPattern { /**
* 姓名
*/
private String name; /**
* 年龄
*/
private int age; /**
* 性别
*/
private Gender gender; public static BuilderPattern newBuilder() {
return new BuilderPattern();
} public BuilderPattern setName(String name) {
this.name = name;
return this;
} public BuilderPattern setAge(int age) {
this.age = age;
return this;
} public BuilderPattern setGender(Gender gender) {
this.gender = gender;
return this;
} @Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
} enum Gender {
MALE, FEMALE
} public static void main(String[] args) { BuilderPattern bp = BuilderPattern.newBuilder().setAge().setName("zhangsan").setGender(Gender.FEMALE);
system.out.println(bp.toString());
} }
3.6 Guava ListenableFuture接口
我们强烈地建议你在代码中多使用 ListenableFuture 来代替JDK的 Future, 因为:
大多数Futures 方法中需要它。
转到 ListenableFuture 编程比较容易。
Guava提供的通用公共类封装了公共的操作方方法,不需要提供Future和 ListenableFuture 的扩展方法。
创建ListenableFuture实例
首先需要创建 ListeningExecutorService 实例,Guava 提供了专门的方法把JDK中提供 ExecutorService对象转换为 ListeningExecutorService 。然后通过submit方法就可以创建一个ListenableFuture实例了。
代码片段如下:
ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool());
ListenableFuture explosion = service.submit(new Callable() {
public Explosion call() {
return pushBigRedButton();
}
});
Futures.addCallback(explosion, new FutureCallback() {
// we want this handler to run immediately after we push the big red button!
public void onSuccess(Explosion explosion) {
walkAwayFrom(explosion);
}
public void onFailure(Throwable thrown) {
battleArchNemesis(); // escaped the explosion!
}
});
也就是说,对于异步的方法,我可以通过监听器来根据执行结果来判断接下来的处理行为。
ListenableFuture 链式操作
使用ListenableFuture 最重要的理由是它可以进行一系列的复杂链式的异步操作。
一般,使用AsyncFunction来完成链式异步操作。不同的操作可以在不同的Executors中执行,单独的ListenableFuture 可以有多个操作等待。
>
Tips: AsyncFunction接口常被用于当我们想要异步的执行转换而不造成线程阻塞时,尽管Future.get()方法会在任务没有完成时造成阻塞,但是AsyncFunction接口并不被建议用来异步的执行转换,它常被用于返回Future实例。
下面给出这个链式操作完成一个简单的异步字符串转换操作:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import com.google.common.util.concurrent.AsyncFunction;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors; /**
* @author tao.ke Date: 14-12-26 Time: 下午5:28
* @version \$Id$
*/
public class ListenerFutureChain { private static final ExecutorService executor = Executors.newFixedThreadPool(); private static final ListeningExecutorService executorService = MoreExecutors.listeningDecorator(executor); public void executeChain() { AsyncFunction<String, String> asyncFunction = new AsyncFunction<String, String>() { @Override
public ListenableFuture<String> apply(final String input) throws Exception {
ListenableFuture<String> future = executorService.submit(new Callable<String>() {
@Override
public String call() throws Exception {
System.out.println("STEP1 >>>" + Thread.currentThread().getName());
return input + "|||step 1 ===--===||| ";
}
}); return future; }
}; AsyncFunction<String, String> asyncFunction2 = new AsyncFunction<String, String>() { @Override
public ListenableFuture<String> apply(final String input) throws Exception {
ListenableFuture<String> future = executorService.submit(new Callable<String>() {
@Override
public String call() throws Exception {
System.out.println("STEP2 >>>" + Thread.currentThread().getName());
return input + "|||step 2 ===--===---||| ";
}
}); return future; }
}; ListenableFuture startFuture = executorService.submit(new Callable() {
@Override
public Object call() throws Exception {
System.out.println("BEGIN >>>" + Thread.currentThread().getName());
return "BEGIN--->";
}
}); ListenableFuture future = Futures.transform(startFuture, asyncFunction, executor);
ListenableFuture endFuture = Futures.transform(future, asyncFunction2, executor); Futures.addCallback(endFuture, new FutureCallback() {
@Override
public void onSuccess(Object result) {
System.out.println(result);
System.out.println("=======OK=======");
} @Override
public void onFailure(Throwable t) {
t.printStackTrace(); }
}); } public static void main(String[] args) {
System.out.println("========START=======");
System.out.println("MAIN >>>" + Thread.currentThread().getName());
ListenerFutureChain chain = new ListenerFutureChain();
chain.executeChain();
System.out.println("========END======="); executor.shutdown();
// System.exit(0);
}
}
输出: ========START=======
MAIN >>>main
BEGIN >>>pool--thread-
========END=======
STEP1 >>>pool--thread-
STEP2 >>>pool--thread-
BEGIN--->|||step ===--===||| |||step ===--===---|||
=======OK=======
从输出可以看出,代码是异步完成字符串操作的。
CacheBuilder实现
写过Cache的,或者其他一些工具类的同学知道,为了让工具类更灵活,我们需要对外提供大量的参数配置给使用者设置,虽然这带有一些好处,但是由于参数太多,使用者开发构造对象的时候过于繁杂。
上面提到过参数配置过多,可以使用Builder模式。Guava Cache也一样,它为我们提供了CacheBuilder工具类来构造不同配置的Cache实例。但是,和本文上面提到的构造器实现有点不一样,它构造器返回的是另外一个对象,因此,这意味着在实现的时候,对象构造函数需要有Builder参数提供配置属性。
4.1 CacheBuilder构造LocalCache实现
首先,我们先看看Cache的构造函数:
/**
* 从builder中获取相应的配置参数。
*/ LocalCache(CacheBuilder<? super K, ? super V> builder, @Nullable CacheLoader<? super K, V> loader) {
concurrencyLevel = Math.min(builder.getConcurrencyLevel(), MAX_SEGMENTS); keyStrength = builder.getKeyStrength();
valueStrength = builder.getValueStrength(); keyEquivalence = builder.getKeyEquivalence();
valueEquivalence = builder.getValueEquivalence(); maxWeight = builder.getMaximumWeight();
weigher = builder.getWeigher();
expireAfterAccessNanos = builder.getExpireAfterAccessNanos();
expireAfterWriteNanos = builder.getExpireAfterWriteNanos();
refreshNanos = builder.getRefreshNanos(); removalListener = builder.getRemovalListener();
removalNotificationQueue = (removalListener == NullListener.INSTANCE) ? LocalCache
.<RemovalNotification<K, V>> discardingQueue() : new ConcurrentLinkedQueue<RemovalNotification<K, V>>(); ticker = builder.getTicker(recordsTime());
entryFactory = EntryFactory.getFactory(keyStrength, usesAccessEntries(), usesWriteEntries());
globalStatsCounter = builder.getStatsCounterSupplier().get();
defaultLoader = loader; int initialCapacity = Math.min(builder.getInitialCapacity(), MAXIMUM_CAPACITY);
if (evictsBySize() && !customWeigher()) {
initialCapacity = Math.min(initialCapacity, (int) maxWeight);
} //.......
}
从构造函数可以看到,Cache的所有参数配置都是从Builder对象中获取的,Builder完成了作为该模式最典型的应用,多配置参数构建对象。
在Cache中只提供一个构造函数,但是在上面代码示例中,我们演示了两种构建缓存的方式:自动加载;手动加载。那么,一般会存在一个完成两者之间的过渡 adapter 组件,接下来看看Builder在内部是如何完成创建缓存对象过程的。
OK,你猜到了。在 LocalCache 中确实提供了两种过渡类,一个是支持自动加载value的 LocalLoadingCache 和只能在键值找不到的时候手动调用获取值方法的 LocalManualCache 。
LocalManualCache实现
static class LocalManualCache<K, V> implements Cache<K, V>, Serializable {
final LocalCache<K, V> localCache; LocalManualCache(CacheBuilder<? super K, ? super V> builder) {
this(new LocalCache<K, V>(builder, null));
} private LocalManualCache(LocalCache<K, V> localCache) {
this.localCache = localCache;
} // Cache methods @Override
@Nullable
public V getIfPresent(Object key) {
return localCache.getIfPresent(key);
} @Override
public V get(K key, final Callable<? extends V> valueLoader) throws ExecutionException {
checkNotNull(valueLoader);
return localCache.get(key, new CacheLoader<Object, V>() {
@Override
public V load(Object key) throws Exception {
return valueLoader.call();
}
});
} //...... @Override
public CacheStats stats() {
SimpleStatsCounter aggregator = new SimpleStatsCounter();
aggregator.incrementBy(localCache.globalStatsCounter);
for (Segment<K, V> segment : localCache.segments) {
aggregator.incrementBy(segment.statsCounter);
}
return aggregator.snapshot();
} // Serialization Support private static final long serialVersionUID = ; Object writeReplace() {
return new ManualSerializationProxy<K, V>(localCache);
}
}
从代码实现看出实际上是一个adapter组件,并且绝大部分实现都是直接调用LocalCache的方法,或者加一些参数判断和聚合。在它核心的构造函数中,就是直接调用LocalCache构造函数,对于loader对象直接设null值。
LocalLoadingCache实现
LocalLoadingCache 实现继承了``类,其主要对get相关方法做了重写。
static class LocalLoadingCache<K, V> extends LocalManualCache<K, V> implements LoadingCache<K, V> { LocalLoadingCache(CacheBuilder<? super K, ? super V> builder, CacheLoader<? super K, V> loader) {
super(new LocalCache<K, V>(builder, checkNotNull(loader)));
} // LoadingCache methods @Override
public V get(K key) throws ExecutionException {
return localCache.getOrLoad(key);
} @Override
public V getUnchecked(K key) {
try {
return get(key);
} catch (ExecutionException e) {
throw new UncheckedExecutionException(e.getCause());
}
} @Override
public ImmutableMap<K, V> getAll(Iterable<? extends K> keys) throws ExecutionException {
return localCache.getAll(keys);
} @Override
public void refresh(K key) {
localCache.refresh(key);
} @Override
public final V apply(K key) {
return getUnchecked(key);
} // Serialization Support private static final long serialVersionUID = ; @Override
Object writeReplace() {
return new LoadingSerializationProxy<K, V>(localCache);
}
}
}
提供了这些adapter类之后,builder类就可以创建 LocalCache ,如下: // 获取value可以通过key计算出
public <K1 extends K, V1 extends V> LoadingCache<K1, V1> build(CacheLoader<? super K1, V1> loader) {
checkWeightWithWeigher();
return new LocalCache.LocalLoadingCache<K1, V1>(this, loader);
} // 手动加载
public <K1 extends K, V1 extends V> Cache<K1, V1> build() {
checkWeightWithWeigher();
checkNonLoadingCache();
return new LocalCache.LocalManualCache<K1, V1>(this);
}
4.2 CacheBuilder参数设置
CacheBuilder 在为我们提供了构造一个Cache对象时,会构造各个成员对象的初始值(默认值)。了解这些默认值,对于我们分析Cache源码实现时,一些判断条件的设置原因,还是很有用的。
初始参数值设置
在 ConcurrentHashMap 中,我们知道有个并发水平(CONCURRENCY_LEVEL),这个参数决定了其允许多少个线程并发操作修改该数据结构。这是因为这个参数是最后map使用的segment个数,而每个segment对应一个锁,因此,对于一个map来说,并发环境下,理论上最大可以有segment个数的线程同时安全地操作修改数据结构。那么是不是segment的值可以设置很大呢?显然不是,要记住维护一个锁的成本还是挺高的,此外如果涉及全表操作,那么性能就会非常不好了。
其他一些初始参数值的设置如下所示:
private static final int DEFAULT_INITIAL_CAPACITY = ; // 默认的初始化Map大小
private static final int DEFAULT_CONCURRENCY_LEVEL = ; // 默认并发水平
private static final int DEFAULT_EXPIRATION_NANOS = ; // 默认超时
private static final int DEFAULT_REFRESH_NANOS = ; // 默认刷新时间
static final int UNSET_INT = -;
boolean strictParsing = true;
int initialCapacity = UNSET_INT;
int concurrencyLevel = UNSET_INT;
long maximumSize = UNSET_INT;
long maximumWeight = UNSET_INT;
long expireAfterWriteNanos = UNSET_INT;
long expireAfterAccessNanos = UNSET_INT;
long refreshNanos = UNSET_INT;
初始对象引用设置
在Cache中,我们除了超时时间,键值引用属性等设置外,还关注命中统计情况,这就需要统计对象来工作。CacheBuilder提供了初始的null 统计对象和空统计对象。
此外,还会设置到默认的引用类型等设置,代码如下:
/**
* 默认空的缓存命中统计类
*/
static final Supplier<? extends StatsCounter> NULL_STATS_COUNTER = Suppliers.ofInstance(new StatsCounter() { //......省略空override @Override
public CacheStats snapshot() {
return EMPTY_STATS;
}
});
static final CacheStats EMPTY_STATS = new CacheStats(, , , , , );// 初始状态的统计对象 /**
* 系统实现的简单的缓存状态统计类
*/
static final Supplier<StatsCounter> CACHE_STATS_COUNTER = new Supplier<StatsCounter>() {
@Override
public StatsCounter get() {
return new SimpleStatsCounter();//这里构造简单地统计类实现
}
}; /**
* 自定义的空RemovalListener,监听到移除通知,默认空处理。
*/
enum NullListener implements RemovalListener<Object, Object> {
INSTANCE; @Override
public void onRemoval(RemovalNotification<Object, Object> notification) {
}
} /**
* 默认权重类,任何对象的权重均为1
*/
enum OneWeigher implements Weigher<Object, Object> {
INSTANCE; @Override
public int weigh(Object key, Object value) {
return ;
}
} static final Ticker NULL_TICKER = new Ticker() {
@Override
public long read() {
return ;
}
}; /**
* 默认的key等同判断
* @return
*/
Equivalence<Object> getKeyEquivalence() {
return firstNonNull(keyEquivalence, getKeyStrength().defaultEquivalence());
} /**
* 默认value的等同判断
* @return
*/
Equivalence<Object> getValueEquivalence() {
return firstNonNull(valueEquivalence, getValueStrength().defaultEquivalence());
} /**
* 默认的key引用
* @return
*/
Strength getKeyStrength() {
return firstNonNull(keyStrength, Strength.STRONG);
} /**
* 默认为Strong 属性的引用
* @return
*/
Strength getValueStrength() {
return firstNonNull(valueStrength, Strength.STRONG);
} <K1 extends K, V1 extends V> Weigher<K1, V1> getWeigher() {
return (Weigher<K1, V1>) Objects.firstNonNull(weigher, OneWeigher.INSTANCE);
}
其中,在我们不设置缓存中键值引用的情况下,默认都是采用强引用及相对应的属性策略来初始化的。此外,在上面代码中还可以看到,统计类 SimpleStatsCounter 是一个简单的实现。里面主要是简单地缓存累加,此外由于多线程下Long类型的线程非安全性,所以也进行了一下封装,下面给出命中率的实现:
public static final class SimpleStatsCounter implements StatsCounter { private final LongAddable hitCount = LongAddables.create();
private final LongAddable missCount = LongAddables.create();
private final LongAddable loadSuccessCount = LongAddables.create();
private final LongAddable loadExceptionCount = LongAddables.create();
private final LongAddable totalLoadTime = LongAddables.create();
private final LongAddable evictionCount = LongAddables.create(); public SimpleStatsCounter() {}
@Override
public void recordHits(int count) {
hitCount.add(count);
}
@Override
public CacheStats snapshot() {
return new CacheStats(
hitCount.sum());
} /**
* Increments all counters by the values in {@code other}.
*/
public void incrementBy(StatsCounter other) {
CacheStats otherStats = other.snapshot();
hitCount.add(otherStats.hitCount());
}
}
因此,CacheBuilder的一些参数对象等得初始化就完成了。可以看到这些默认的初始化,有两套引用:Null对象和Empty对象,显然Null会更省空间,但我们在创建的时候将决定不使用某特性的时候,就会使用Null来创建,否则使用Empty来完成初始化工作。在分析Cache的时候,写后超时队列和读后超时队列也存在两个版本。
LocalCache实现
在设计实现上, LocalCache 的并发策略和 concurrentHashMap 的并发策略是一致的,也是根据分段锁来提高并发能力,分段锁可以很好的保证 并发读写的效率。因此,该map支持非阻塞读和不同段之间并发写。
如果最大的大小指定了,那么基于段来执行操作是最好的。使用页面替换算法来决定当map大小超过指定值时,哪些entries需要被驱赶出去。页面替换算法的数据结构保证Map临时一致性:对一个segment写排序是一致的;但是对map进行更新和读不能直接立刻 反应在数据结构上。 虽然这些数据结构被lock锁保护,但是其结构决定了批量操作可以避免锁竞争出现。在线程之间传播的批量操作导致分摊成本比不强制大小限制的操作要稍微高一点。
此外, LoacalCache 使用LRU页面替换算法,是因为该算法简单,并且有很高的命中率,以及O(1)的时间复杂度。需要说明的是, LRU算法是基于页面而不是全局实现的,所以可能在命中率上不如全局LRU算法,但是应该基本相似。
最后,要说明一点,在代码实现上,页面其实就是一个段segment。之所以说page页,是因为在计算机专业课程上,CPU,操作系统,算法上,基本上都介绍过分页导致优化效果的提升。
从图中可以直观看到cache是以segment粒度来控制并发get和put等操作的,接下来首先看我们的 LocalCache 是如何构造这些segment段的,继续上面初始化localCache构造函数的代码:
// 找到大于并发水平的最小2的次方的值,作为segment数量
int segmentShift = ;
int segmentCount = ;
while (segmentCount < concurrencyLevel && (!evictsBySize() || segmentCount * <= maxWeight)) {
++segmentShift;
segmentCount <<= ;
}
this.segmentShift = - segmentShift;//位 偏移数
segmentMask = segmentCount - ;//mask码 this.segments = newSegmentArray(segmentCount);// 构造数据数组,如上图所示
//获取每个segment初始化容量,并且保证大于等于map初始容量
int segmentCapacity = initialCapacity / segmentCount;
if (segmentCapacity * segmentCount < initialCapacity) {
++segmentCapacity;
} //段Size 必须为2的次数,并且刚刚大于段初始容量
int segmentSize = ;
while (segmentSize < segmentCapacity) {
segmentSize <<= ;
} // 权重设置,确保权重和==map权重
if (evictsBySize()) {
// Ensure sum of segment max weights = overall max weights
long maxSegmentWeight = maxWeight / segmentCount + ;
long remainder = maxWeight % segmentCount;
for (int i = ; i < this.segments.length; ++i) {
if (i == remainder) {
maxSegmentWeight--;
}
//构造每个段结构
this.segments[i] = createSegment(segmentSize, maxSegmentWeight, builder.getStatsCounterSupplier().get());
}
} else {
for (int i = ; i < this.segments.length; ++i) {
//构造每个段结构
this.segments[i] = createSegment(segmentSize, UNSET_INT, builder.getStatsCounterSupplier().get());
}
}
Notes:基本上都是基于2的次数来设置大小的,显然基于移位操作比普通计算操作速度要快。此外,对于最大权重分配到段权重的设计上,很特殊。为什么呢?为了保证两者能够相等(maxWeight==sumAll(maxSegmentWeight)),对于remainder前面的segment maxSegmentWeight的值比remainder后面的权重值大1,这样保证最后值相等。 map get 方法 @Override
@Nullable
public V get(@Nullable Object key) {
if (key == null) {
return null;
}
int hash = hash(key);
return segmentFor(hash).get(key, hash);
}
Notes:代码很简单,首先check key是否为null,然后计算hash值,定位到对应的segment,执行segment实例拥有的get方法获取对应的value值 map put 方法 @Override
public V put(K key, V value) {
checkNotNull(key);
checkNotNull(value);
int hash = hash(key);
return segmentFor(hash).put(key, hash, value, false);
}
Notes:和get方法一样,也是先check值,然后计算key的hash值,然后定位到对应的segment段,执行段实例的put方法,将键值存入map中。 map isEmpty 方法 @Override
public boolean isEmpty() {
long sum = 0L;
Segment<K, V>[] segments = this.segments;
for (int i = ; i < segments.length; ++i) {
if (segments[i].count != ) {
return false;
}
sum += segments[i].modCount;
} if (sum != 0L) { // recheck unless no modifications
for (int i = ; i < segments.length; ++i) {
if (segments[i].count != ) {
return false;
}
sum -= segments[i].modCount;
}
if (sum != 0L) {
return false;
}
}
return true;
}
Notes:判断Cache是否为空,就是分别判断每个段segment是否都为空,但是由于整体是在并发环境下进行的,也就是说存在对一个segment并发的增加和移除元素的时候,而我们此时正在check其他segment段。
上面这种情况,决定了我们不能够获得任何一个时间点真实段状态的情况。因此,上面的代码引入了sum变量来计算段modCount变更情况。modCount表示改变segment大小size的更新次数,这个在批量读取方法期间保证它们可以看到一致性的快照。 需要注意,这里先获取count,该值是volatile,因此modCount通常都可以在不需要一致性控制下,获得当前segment最新的值.
在判断如果在第一次check的时候,发现segment发生了数据结构级别变更,则会进行recheck,就是在每个modCount下,段仍然是空的,则判断该map为空。如果发现这期间数据结构发生变化,则返回非空判断。
map 其他方法
在Cache数据结构中,还有很多方法,和上面列出来的方法一样,其底层核心实现都是依赖segment类实例中实现的对应方法。
此外,在总的数据结构中,还提供了一些根据builder配制制定相应地缓存策略方法。比如:
expiresAfterAccess:是否执行访问后超时过期策略;
expiresAfterWrite:是否执行写后超时过期策略;
usesAccessQueue:根据上面的配置决定是否需要new一个访问队列;
usesWriteQueue:根据上面的配置决定是否需要new一个写队列;
usesKeyReferences/usesValueReferences:是否需要使用特别的引用策略(非Strong引用).
等等……
5.2 引用数据结构
在介绍Segment数据结构之前,先讲讲Cache中引用的设计。
关于Reference引用的一些说明,在博文的上面已经介绍了,这里就不赘述。在Guava Cache 中,主要使用三种引用类型,分别是: STRONG引用 , SOFT引用 , WEAK引用 。和Map不同,在Cache中,使用 ReferenceEntry 来封装键值对,并且对于值来说,还额外实现了 ValueReference 引用对象来封装对应Value对象。
ReferenceEntry节点结构
为了支持各种不同类型的引用,以及不同过期策略,这里构造了一个ReferenceEntry节点结构。通过下面的节点数据结构,可以清晰的看到缓存大致操作流程。
/**
* 引用map中一个entry节点。
*
* 在map中得entries节点有下面几种状态:
* valid:-live:设置了有效的key/value;-loading:加载正在处理中....
* invalid:-expired:时间过期(但是key/value可能仍然设置了);Collected:key/value部分被垃圾收集了,但是还没有被清除;
* -unset:标记为unset,表示等待清除或者重新使用。
*
*/
interface ReferenceEntry<K, V> {
/**
* 从entry中返回value引用
*/
ValueReference<K, V> getValueReference(); /**
* 为entry设置value引用
*/
void setValueReference(ValueReference<K, V> valueReference); /**
* 返回链中下一个entry(解决hash碰撞存在链表)
*/
@Nullable
ReferenceEntry<K, V> getNext(); /**
* 返回entry的hash
*/
int getHash(); /**
* 返回entry的key
*/
@Nullable
K getKey(); /*
* Used by entries that use access order. Access entries are maintained in a doubly-linked list. New entries are
* added at the tail of the list at write time; stale entries are expired from the head of the list.
*/ /**
* 返回该entry最近一次被访问的时间ns
*/
long getAccessTime(); /**
* 设置entry访问时间ns.
*/
void setAccessTime(long time); /**
* 返回访问队列中下一个entry
*/
ReferenceEntry<K, V> getNextInAccessQueue(); /**
* Sets the next entry in the access queue.
*/
void setNextInAccessQueue(ReferenceEntry<K, V> next); /**
* Returns the previous entry in the access queue.
*/
ReferenceEntry<K, V> getPreviousInAccessQueue(); /**
* Sets the previous entry in the access queue.
*/
void setPreviousInAccessQueue(ReferenceEntry<K, V> previous); // ...... 省略write队列相关方法,和access一样
}
Notes:从上面代码可以看到除了和Map一样,有key、value、hash和next四个属性之外,还有访问和写更新两个双向链表队列,以及entry的最近访问时间和最近更新时间。显然,多出来的属性就是为了支持缓存必须要有的过期机制。
此外,从上面的代码可以看出 cache支持的LRU机制实际上是建立在segment上的,也就是基于页的替换机制。
关于访问队列数据结构,其实质就是一个双向的链表。当节点被访问的时候,这个节点将会移除,然后把这个节点添加到链表的结尾。关于具体实现,将在segment中介绍。
创建不同类型的ReferenceEntry由其枚举工厂类EntryFactory来实现,它根据key的Strength类型、是否使用accessQueue、是否使用writeQueue来决定不同的EntryFactry实例,并通过它创建相应的ReferenceEntry实例
ValueReference结构
同样为了支持Cache中各个不同类型的引用,其对Value类型进行再封装,支持引用。看看其内部数据属性:
/**
* A reference to a value.
*/
interface ValueReference<K, V> {
/**
* Returns the value. Does not block or throw exceptions.
*/
@Nullable
V get(); /**
* Waits for a value that may still be loading. Unlike get(), this method can block (in the case of
* FutureValueReference).
*
* @throws ExecutionException if the loading thread throws an exception
* @throws ExecutionError if the loading thread throws an error
*/
V waitForValue() throws ExecutionException; /**
* Returns the weight of this entry. This is assumed to be static between calls to setValue.
*/
int getWeight(); /**
* Returns the entry associated with this value reference, or {@code null} if this value reference is
* independent of any entry.
*/
@Nullable
ReferenceEntry<K, V> getEntry(); /**
* 为一个指定的entry创建一个该引用的副本
* <p>
* {@code value} may be null only for a loading reference.
*/
ValueReference<K, V> copyFor(ReferenceQueue<V> queue, @Nullable V value, ReferenceEntry<K, V> entry); /**
* 告知一个新的值正在加载中。这个只会关联到加载值引用。
*/
void notifyNewValue(@Nullable V newValue); /**
* 当一个新的value正在被加载的时候,返回true。不管是否已经有存在的值。这里加锁方法返回的值对于给定的ValueReference实例来说是常量。
*
*/
boolean isLoading(); /**
* 返回true,如果该reference包含一个活跃的值,意味着在cache里仍然有一个值存在。活跃的值包含:cache查找返回的,等待被移除的要被驱赶的值; 非激活的包含:正在加载的值,
*/
boolean isActive();
}
Notes:value引用接口对象中包含了不同状态的标记,以及一些加载方法和获取具体value值对象。
为了减少不必须的load加载,在value引用中增加了loading标识和wait方法等待加载获取值。这样,就可以等待上一个调用loader方法获取值,而不是重复去调用loader方法加重系统负担,而且可以更快的获取对应的值。
此外,介绍下 ReferenceQueue 引用队列,这个队列是JDK提供的,在检测到适当的可到达性更改后,垃圾回收器将已注册的引用对象添加到该队列中。因为Cache使用了各种引用,而通过ReferenceQueue这个“监听器”就可以优雅的实现自动删除那些引用不可达的key了,是不是很吊,哈哈。
在Cache分别实现了基于Strong,Soft,Weak三种形式的ValueReference实现。
这里ValueReference之所以要有对ReferenceEntry的引用是因为在Value因为WeakReference、SoftReference被回收时,需要使用其key将对应的项从Segment段中移除;
copyFor()函数的存在是因为在expand(rehash)重新创建节点时,对WeakReference、SoftReference需要重新创建实例(C++中的深度复制思想,就是为了保持对象状态不会相互影响),而对强引用来说,直接使用原来的值即可,这里很好的展示了对彼变化的封装思想;
notifiyNewValue只用于LoadingValueReference,它的存在是为了对LoadingValueReference来说能更加及时的得到CacheLoader加载的值。
5.3 Segment 数据结构
Segment 数据结构,是ConcurrentHashMap的核心实现,也是该结构保证了其算法的高效性。在 Guava Cache 中也一样, segment 数据结构保证了缓存在线程安全的前提下可以高效地更新,插入,获取对应value。
实际上,segment就是一个特殊版本的hash table实现。其内部也是对应一个锁,不同的是,对于get和put操作做了一些优化处理。因此,在代码实现的时候,为了快速开发和利用已有锁特性,直接 extends ReentrantLock 。
在segment中,其主要的类属性就是一个 LoacalCache 类型的map变量。关于segment实现说明,如下:
/**
* segments 维护一个entry列表的table,确保一致性状态。所以可以不加锁去读。节点的next field是不可修改的final,因为所有list的增加操作
* 是执行在每个容器的头部。因此,这样子很容易去检查变化,也可以快速遍历。此外,当节点被改变的时候,新的节点将被创建然后替换它们。 由于容器的list一般都比较短(平均长度小于2),所以对于hash
* tables来说,可以工作的很好。虽然说读操作因此可以不需要锁进行,但是是依赖
* 使用volatile确保其他线程完成写操作。对于绝大多数目的而言,count变量,跟踪元素的数量,其作为一个volatile变量确保可见性(其内部原理可以参考其他相关博文)。
* 这样一下子变得方便的很多,因为这个变量在很多读操作的时候都会被获取:所有非同步的(unsynchronized)读操作必须首先读取这个count值,并且如果count为0则不会 查找table
* 的entries元素;所有的同步(synchronized)操作必须在结构性的改变任务bin容器之后,才会写操作这个count值。
* 这些操作必须在并发读操作看到不一致的数据的时候,不采取任务动作。在map中读操作性质可以更容易实现这个限制。例如:没有操作可以显示出 当table
* 增长了,但是threshold值没有更新,所以考虑读的时候不要求原子性。作为一个原则,所有危险的volatile读和写count变量都必须在代码中标记。
*/ final LocalCache<K, V> map; /**
* 该segment区域内所有存活的元素个数
*/
volatile int count; /**
* 改变table大小size的更新次数。这个在批量读取方法期间保证它们可以看到一致性的快照:
* 如果modCount在我们遍历段加载大小或者核对containsValue期间被改变了,然后我们会看到一个不一致的状态视图,以至于必须去重试。
* count+modCount 保证内存一致性
*
* 感觉这里有点像是版本控制,比如数据库里的version字段来控制数据一致性
*/
int modCount; /**
* 每个段表,使用乐观锁的Array来保存entry The per-segment table.
*/
volatile AtomicReferenceArray<ReferenceEntry<K, V>> table; // 这里和concurrentHashMap不一致,原因是这边元素是引用,直接使用不会线程安全
/**
* A queue of elements currently in the map, ordered by write time. Elements are added to the tail of the queue
* on write.
*/
@GuardedBy("Segment.this")
final Queue<ReferenceEntry<K, V>> writeQueue; /**
* A queue of elements currently in the map, ordered by access time. Elements are added to the tail of the queue
* on access (note that writes count as accesses).
*/
@GuardedBy("Segment.this")
final Queue<ReferenceEntry<K, V>> accessQueue;
Notes:
在segment实现中,很多地方使用count变量和modCount变量来保持线程安全,从而省掉lock开销。
在本文上面的图中说明了每个segment就是一个节点table,和jdk实现不一致,这里为了GC,内部维护的是一个 AtomicReferenceArray<ReferenceEntry<K, V>> 类型的列表,可以保证安全性。
最后, LocalCache 作为一个缓存,其必须具有访问和写超时特性,因为其内部维护了访问队列和写队列,队列中的元素按照访问或者写时间排序,新的元素会被添加到队列尾部。如果,在队列中已经存在了该元素,则会先delete掉,然后再尾部add该节点,新的时间。这也就是为什么,对于 LocalCache 而言,其LRU是针对segment的,而不是全Cache范围的。
在本文的 5.2节中知道,cache会根据初始化实例时配置来创建多个segment( createSegment ),然后该方法最终调用segment类的构造函数创建一个段。对于参数set,就不展示,看看构造方法中其主要操作:
// 构造函数
Segment(LocalCache<K, V> map, int initialCapacity, long maxSegmentWeight, StatsCounter statsCounter) {
initTable(newEntryArray(initialCapacity));
} AtomicReferenceArray<ReferenceEntry<K, V>> newEntryArray(int size) {
return new AtomicReferenceArray<ReferenceEntry<K, V>>(size);
} void initTable(AtomicReferenceArray<ReferenceEntry<K, V>> newTable) {
this.threshold = newTable.length() * / ; // 0.75
if (!map.customWeigher() && this.threshold == maxSegmentWeight) {
// prevent spurious expansion before eviction
this.threshold++;
}
this.table = newTable;
}
OK,这里我们已经构造好了整个localCache对象了,包括其内部每个segment中对应的节点表。这些节点table,决定了最后所有核心操作的具体实现和操作结果。
接下来,需要看看最核心的几个方法。
Tips:本文把这几个方法单独作为几节来说明,这也表示这几个方法的重要性。
Notes:上面从缓存中直接获取key对应value,是完全没有加锁来完成的。
scheduleRefresh方法
如果配置refresh特性,到了配置的刷新间隔时间,而且节点也没有正在加载,则应该进行refresh操作。refresh操作比较复杂。
六、Guava Cache中数据的定位 两次Hash 类似于ConcurrentHashMap
其实 Guava Cache 为了满足并发场景的使用,核心的数据结构就是按照 ConcurrentHashMap 来的,这里也是一个 key 定位到一个具体位置的过程。
先找到 Segment,再找具体的位置,等于是做了两次 Hash 定位。
主要的类:
CacheBuilder 设置参数,构建Cache
LocalCache 是核心实现,虽然builder构建的是LocalLoadingCache(带refresh功能)和LocalManualCache(不带refresh功能),但其实那两个只是个壳子
1、CacheBuilder 构建器
提要:
记录所需参数
public final class CacheBuilder<K, V> { public <K1 extends K, V1 extends V> LoadingCache<K1, V1> build(
CacheLoader<? super K1, V1> loader) { // loader是用来自动刷新的
checkWeightWithWeigher();
return new LocalCache.LocalLoadingCache<>(this, loader);
} public <K1 extends K, V1 extends V> Cache<K1, V1> build() { // 这个没有loader,就不会自动刷新
checkWeightWithWeigher();
checkNonLoadingCache();
return new LocalCache.LocalManualCache<>(this);
} int initialCapacity = UNSET_INT; // 初始map大小
int concurrencyLevel = UNSET_INT; // 并发度
long maximumSize = UNSET_INT;
long maximumWeight = UNSET_INT;
Weigher<? super K, ? super V> weigher;
Strength keyStrength; // key强、弱、软引,默认为强
Strength valueStrength; // value强、弱、软引,默认为强
long expireAfterWriteNanos = UNSET_INT; // 写过期
long expireAfterAccessNanos = UNSET_INT; //
long refreshNanos = UNSET_INT; //
Equivalence<Object> keyEquivalence; // 强引时为equals,否则为==
Equivalence<Object> valueEquivalence; // 强引时为equals,否则为==
RemovalListener<? super K, ? super V> removalListener; // 删除时的监听
Ticker ticker; // 时间钟,用来获得当前时间的
Supplier<? extends StatsCounter> statsCounterSupplier = NULL_STATS_COUNTER; // 计数器,用来记录get或者miss之类的数据
}
2、LocalCache
在上文的分析中可以看出 Cache 中的 ReferenceEntry
是类似于 HashMap 的 Entry 存放数据的。
来看看 ReferenceEntry 的定义:
interface ReferenceEntry<K, V> { allBackListener {
/**
* Returns the value reference from this entry.
*/
ValueReference<K, V> getValueReference(); /** ify(String msg) ;
* Sets the value reference for this entry.
*/
void setValueReference(ValueReference<K, V> valueReference); /**
* Returns the next entry in the chain.
*/
@Nullable
ReferenceEntry<K, V> getNext(); /**
* Returns the entry's hash.
*/
int getHash(); /**
* Returns the key for this entry.
*/
@Nullable
K getKey(); /*
* Used by entries that use access order. Access entries are maintained in a doubly-linked list.
* New entries are added at the tail of the list at write time; stale entries are expired from
* the head of the list.
*/ /**
* Returns the time that this entry was last accessed, in ns.
*/
long getAccessTime(); /**
* Sets the entry access time in ns.
*/
void setAccessTime(long time); }
包含了很多常用的操作,如值引用、键引用、访问时间等。
根据 ValueReference<K, V> getValueReference();
的实现:
具有强引用和弱引用的不同实现。
key 也是相同的道理:
当使用这样的构造方式时,弱引用的 key 和 value 都会被垃圾回收。
当然我们也可以显式的回收:
/**
* Discards any cached value for key {@code key}.
* 单个回收
*/
void invalidate(Object key); /**
* Discards any cached values for keys {@code keys}.
*
* @since 11.0
*/
void invalidateAll(Iterable<?> keys); /**
* Discards all entries in the cache.
*/
void invalidateAll();
回调
改造了之前的例子:
loadingCache = CacheBuilder.newBuilder()
.expireAfterWrite(, TimeUnit.SECONDS)
.removalListener(new RemovalListener<Object, Object>() {
@Override
public void onRemoval(RemovalNotification<Object, Object> notification) {
LOGGER.info("删除原因={},删除 key={},删除 value={}",notification.getCause(),notification.getKey(),notification.getValue());
}
})
.build(new CacheLoader<Integer, AtomicLong>() {
@Override
public AtomicLong load(Integer key) throws Exception {
return new AtomicLong();
}
});
执行结果:
-- ::07.433 [main] INFO c.crossoverjie.guava.CacheLoaderTest - 当前缓存值=,缓存大小=
-- ::07.442 [main] INFO c.crossoverjie.guava.CacheLoaderTest - 缓存的所有内容={=}
-- ::07.443 [main] INFO c.crossoverjie.guava.CacheLoaderTest - job running times=
-- ::10.461 [main] INFO c.crossoverjie.guava.CacheLoaderTest - 删除原因=EXPIRED,删除 key=,删除 value=
-- ::10.462 [main] INFO c.crossoverjie.guava.CacheLoaderTest - 当前缓存值=,缓存大小=
-- ::10.462 [main] INFO c.crossoverjie.guava.CacheLoaderTest - 缓存的所有内容={=}
可以看出当缓存被删除的时候会回调我们自定义的函数,并告知删除原因。
那么 Guava 是如何实现的呢?
根据 LocalCache 中的 getLiveValue()
中判断缓存过期时,跟着这里的调用关系就会一直跟到:
removeValueFromChain()
中的:
enqueueNotification()
方法会将回收的缓存(包含了 key,value)以及回收原因包装成之前定义的事件接口加入到一个本地队列中。
这样一看也没有回调我们初始化时候的事件啊。
不过用过队列的同学应该能猜出,既然这里写入队列,那就肯定就有消费。
我们回到获取缓存的地方:
在 finally 中执行了 postReadCleanup()
方法;其实在这里面就是对刚才的队列进行了消费:
一直跟进来就会发现这里消费了队列,将之前包装好的移除消息调用了我们自定义的事件,这样就完成了一次事件回调。
8、Guava Cache特性:对于同一个key,只让一个请求回源load数据,其他线程阻塞等待结果 (问题4 如何做到的)
KeyReferenceQueue: 基于引用的Entry,其实现类有弱引用Entry,强引用Entry等 ,已经被GC,需要内部清理的键引用队列。
ValueReference
对于ValueReference,因为Guava Cache支持强引用的Value、SoftReference Value以及WeakReference Value,因而它对应三个实现类:StrongValueReference、SoftValueReference、WeakValueReference。为了支持动态加载机制,它还有一个LoadingValueReference,在需要动态加载一个key的值时,先把该值封装在LoadingValueReference中,以表达该key对应的值已经在加载了,如果其他线程也要查询该key对应的值,就能得到该引用,并且等待改值加载完成,从而保证该值只被加载一次(可以在evict以后重新加载)。
在该只加载完成后,将LoadingValueReference替换成其他ValueReference类型。对新创建的LoadingValueReference,由于其内部oldValue的初始值是UNSET,它isActive为false,isLoading为false,因而此时的LoadingValueReference的isActive为false,但是isLoading为true。每个ValueReference都纪录了weight值,所谓weight从字面上理解是“该值的重量”,它由Weighter接口计算而得。weight在Guava Cache中由两个用途:1. 对weight值为0时,在计算因为size limit而evict是忽略该Entry(它可以通过其他机制evict);2. 如果设置了maximumWeight值,则当Cache中weight和超过了该值时,就会引起evict操作。但是目前还不知道这个设计的用途。最后,Guava Cache还定义了Stength枚举类型作为ValueReference的factory类,它有三个枚举值:Strong、Soft、Weak,这三个枚举值分别创建各自的ValueReference,并且根据传入的weight值是否为1而决定是否要创建Weight版本的ValueReference。
设想高并发下的一种场景:假设我们将name=aty存放到缓存中,并设置的有过期时间。当缓存过期后,恰好有10个客户端发起请求,需要读取name的值。使用Guava Cache可以保证只让一个线程去加载数据(比如从数据库中),而其他线程则等待这个线程的返回结果。这样就能避免大量用户请求穿透缓存。
九、总结
1、guava使用的场景:
在平常开发过程中,很多情况需要使用缓存来避免频繁SQL查询或者其他耗时操作,会采取缓存这些操作结果给下一次请求使用。如果我们的操作结果是一直不改变的,其实我们可以使用 ConcurrentHashMap 来存储这些数据;但是如果这些结果在随后时间内会改变或者我们希望存放的数据所占用的内存空间可控,这样就需要自己来实现这种数据结构了。
显然,对于这种十分常见的需求, Guava 提供了自己的工具类实现。 Guava Cache 提供了一般我们使用缓存所需要的几乎所有的功能,主要有:
(1) 自动将entry节点加载进缓存结构中;
(2)当缓存的数据已经超过预先设置的最大值时,使用LRU算法移除一些数据;
(3)具备根据entry节点上次被访问或者写入的时间来计算过期机制;
(4)缓存的key被封装在`WeakReference`引用内;
(5)缓存的value被封装在`WeakReference`或者`SoftReference`引用内;
(6)移除entry节点,可以触发监听器通知事件;
缓存加载:CacheLoader、Callable、显示插入(put)
缓存回收:LRU,定时(expireAfterAccess
,expireAfterWrite
),软弱引用,显示删除(Cache接口方法invalidate
,invalidateAll
)
监听器:CacheBuilder.removalListener(RemovalListener)
清理缓存时间:只有在获取数据时才或清理缓存LRU,使用者可以单起线程采用Cache.cleanUp()
方法主动清理。
刷新:主动刷新方法LoadingCache.referesh(K)
信息统计:CacheBuilder.recordStats()
开启Guava Cache的统计功能。Cache.stats()
返回CacheStats对象。(其中包括命中率等相关信息)
获取当前缓存所有数据:cache.asMap()
,cache.asMap().get(Object)会刷新数据的访问时间(影响的是:创建时设置的在多久没访问后删除数据)
对于Guava Cache 对于其核心实现我会做如下的设计:
- 定义一个CacheConfig类用于纪录所有的配置,如CacheLoader,maximum size、expire time、key reference level、value reference level、eviction listener等。
- 定义一个Cache接口,该接口类似Map(或ConcurrentMap),但是为了和Map区别开来,因而重新定义一个Cache接口。
- 定义一个实现Cache接口的类CacheImpl,它接收CacheConfig作为参数的构造函数,并将CacheConfig实例保存在字段中。
- 在实现上模仿ConcurrentHashMap的实现方式,有一个Segment数组,其长度由配置的concurrencyLevel值决定。为了实现最近最少使用算法(LRU),添加AccessQueue和WriteQueue字段,这两个Queue内部采用双链表,每次新创建一个Entry,就将这个Entry加入到这两个Queue的末尾,而每读取一个Entry就将其添加到AccessQueue的末尾,没更新一个Entry将该Entry添加到WriteQueue末尾。为了实现key和value上的WeakReference、SoftReference,添加ReferenceQueue<K>类型的keyReferenceQueue和valueReferenceQueue字段。
- 在每次调用方法之前都遍历AccessQueue和WriteQueue,如果发现有Entry已经expire,就将该Entry从这两个Queue上和Cache中移除。然后遍历keyReferenceQueue和valueReference,如果发现有项存在,同样将它们移除。在移除时如果有EvictionListener注册着,则调用该listener。
- 对Segment实现,它时一个CacheEntry数组,CacheEntry是一个链节点,它包含hash、key、vlaue、next。CacheEntry根据是否需要包装在WeakReference中创建WeakEntry或StrongEntry,而对value根据是否需要包装在WeakReference、SoftReference中创建WeakValueReference、SoftValueReference、StrongValueReference。在get操作中对于需要使用CacheLoader加载的值先添加一个具有LoadingValueReference值的Entry,这样可以保证同一个Key只加载依次。在加载成功后将LoadingValueReference根据配置替换成其他Weak、Soft、Strong ValueReference。
- 对于cache access statistics,只需要有一个类在需要的地方做一些统计计数即可。
源码参考:guava源码
参考:Guava Cache特性:对于同一个key,只让一个请求回源load数据,其他线程阻塞等待结果