一、ehcahce 何时用比较好;
二、问题:当有个消息的key不在guava里面的话,若是大量的消息过来,会同时请求数据库吗?仍是只有一个请求数据库,其余的等待第一个把数据从DB加载到Guava中html
回答:是的,其余的都会等待load,直到数据加载完毕;
二、recency queue 干吗用的:java
目前没看出来,可是应该是为了LRU队列也就是快速删除算法,由于recency queue的队列,若是读的话,会往recency queue和 access queue中写入数据,若是写的话,首先要清空recency queue队列,而后在recency queue中,而后再在access queue中写入队列;因此应该会为了快速删除过时数据准备的queue:mysql
目前在网安部项目中,会接收到LBS消息 高峰期的QPS大约为5000,目前是直接经过LBS消息的订单ID查询 查询订单接口的数据,因为涉及到上游部署,或者网络抖动的问题,当上游积压时,订单常常会报警。所以考虑对缓存作一次调研。git
在多线程高并发场景中每每是离不开cache的,须要根据不一样的应用场景来须要选择不一样的cache,好比分布式缓存如Redis、memcached,还有本地(进程内)缓存如ehcache、GuavaCachegithub
缓存分为本地缓存和分布式缓存, 为何要使用本地缓存呢?由于本地缓存比IO更高效,比分布式缓存更稳定。。redis
分布式缓存主要为redis或memcached之类的称为分布式缓存,算法
优势:sql
Redis 容量大,能够持久化,能够实现分布式的缓存,能够处理每秒百万级的并发,是专业的缓存服务,数据库
redis可单独部署,多个项目之间能够共享,本地内存没法共享;apache
在多实例的状况下,各实例共用一份缓存数据,缓存具备一致性。
缺点:
须要保持redis或memcached服务的高可用,整个程序架构上较为复杂,硬件成本较高
本地缓存主要为Ecache和 guava Cache
区别:
适用Ehcache的状况
适用Guava cache的状况
Guava cache说简单点就是一个支持LRU的ConCurrentHashMap,它没有Ehcache那么多的各类特性,只是提供了增、删、改、查、刷新规则和时效规则设定等最基本的元素。作一个jar包中的一个功能之一,Guava cache极度简洁并能知足觉大部分人的要求。
愿意花费一部份内存来提升速度 -- 以空间换时间
期待有些关键字会被屡次查询 -- 热点数据
不须要持久化
缓存中存放的数据总量不会超出内存容量。
总结
Ehcache有着全面的缓存特性,可是略重。Guava cache有最基本的缓存特性,很轻。
两种类型都是成熟的缓存框架,因为不须要保存到本地磁盘 考虑到Ehcahce 比较重,而Guava 比较轻量,考虑使用Guava
Guava工程包含了若干被Google的 Java项目普遍依赖 的核心库;Google Guava Cache是一种很是优秀本地缓存解决方案,提供了基于容量,时间和引用的缓存回收方式。
Guava Cache 其核心数据结构大致上和 ConcurrentHashMap 一致,具体细节上会有些区别。功能上,ConcurrentMap会一直保存全部添加的元素,直到显式地移除。相对地, Guava Cache 为了限制内存占用,一般都设定为自动回收元素。在某些场景下,尽管它不回收元素,也是颇有用的,由于它会自动加载缓存。
Guava Cache与java1.7的ConcurrentMap很类似,但也不彻底同样。最基本的区别是ConcurrentMap会一直保存全部添加的元素,直到显式地移除。相对地,Guava Cache为了限制内存占用,一般都设定为自动回收元素。
在这里就会涉及到segement的概念了,咱们先把关系理清楚,首先看ConcurrentHashMap的图示,这样有助于咱们理解:
Guava Cache中的核心类,重点了解。
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):这是一种最弱的引用关系,甚至无法经过引用来获取对象,它惟一的做用就是在被回收时能够得到通知。
基于引用的Entry,其实现类有弱引用Entry,强引用Entry等
已经被GC,须要内部清理的键引用队列。
已经被GC,须要内部清理的值引用队列。
记录升级可访问列表清单时的entries,当segment上达到临界值或发生写操做时该队列会被清空。
按照写入时间进行排序的元素队列,写入一个元素时会把它加入到队列尾部。
按照访问时间进行排序的元素队列,访问(包括写入)一个元素时会把它加入到队列尾部。
table的数据结构 类型为:AtomicReferenceArray
Segment继承于ReetrantLock,减少锁粒度,提升并发效率。
相似于HasmMap中的table同样,至关于entry的容器。
(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以节省内存:
先看一下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一般只须要简单地实现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)
。
这种方式不须要在建立的时候指定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(10) //设置并发数为5,即同一时间最多只能有5个线程往cache执行写入操做 .concurrencyLevel(5) //设置cache中的数据在写入以后的存活时间为10秒 .expireAfterWrite(10, 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(Least Recently Used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“若是数据最近被访问过,那么未来被访问的概率也更高”。
1.新数据插入到链表头部;
2.每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
3.当链表满的时候,将链表尾部的数据丢弃。
Guava Cache中借助读写队列来实现LRU算法。
Guava Cache提供了四种基本的缓存回收方式:(a)基于容量回收、(b)定时回收 (c)基于引用回收 (d)显式清除
maximumSize(long):当缓存中的元素数量超过指定值时。
当缓存个数超过CacheBuilder.maximumSize(long)设置的值时,优先淘汰最近没有使用或者不经常使用的元素。同理
CacheBuilder.maximumWeight(long)也是同样逻辑。
若是要规定缓存项的数目不超过固定值,只需使用CacheBuilder.maximumSize(long)。缓存将尝试回收最近没有使用或整体上不多使用的缓存项。——警告:在缓存项的数目达到限定值以前,缓存就可能进行回收操做——一般来讲,这种状况发生在缓存项的数目逼近限定值时。
b、定时回收(Timed Eviction)
CacheBuilder提供两种定时回收的方法:
(a)按照写入时间,最先写入的最早回收;
expireAfterAccess(long, TimeUnit):缓存项在给定时间内没有被读/写访问,则回收。请注意这种缓存的回收顺序和基于大小回收同样。
(b)按照访问时间,最先访问的最先回收。
expireAfterWrite(long, TimeUnit):缓存项在给定时间内没有被写访问(建立或覆盖),则回收。若是认为缓存数据老是在固定时候后变得陈旧不可用,这种回收方式是可取的。
清理发生时机
使用CacheBuilder构建的缓存不会”自动”执行清理和回收工做,也不会在某个缓存项过时后立刻清理。相反,它会在写操做时顺带作少许的维护工做,或者偶尔在读操做时作——若是写操做实在太少的话。
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;
一、再跟进去以前第 2189 行会发现先要判断 count 是否大于 0,这个 count 保存的是当前Segment中缓存元素的数量,并用 volatile 修饰保证了可见性。
二、根据方法名称能够看出是判断当前的 Entry 是否过时,该 entry 就是经过 key 查询到的。这里就很明显的看出是根据根据构建时指定的过时方式来判断当前 key 是否过时了。
若是过时就往下走,尝试进行过时删除(须要加锁,保证操做此Segment的线程安全)。
获取当前缓存的总数量
自减一(前面获取了锁,因此线程安全)
删除并将更新的总数赋值到 count。
而在查询时候顺带作了这些事情,可是若是该缓存迟迟没有访问也会存在数据不能被回收的状况,不过这对于一个高吞吐的应用来讲也不是问题。
删除包含了两部分:(a)回收弱,软引用queue(b) 删除 access和write队列 中过时时间的数据
(a)回收keyReference 和 valueReference 队列 弱,软引用queue
(b)删除 access和write队列 中过时时间的数据
GuavaCache的工做流程:获取数据->若是存在,返回数据->计算获取数据->存储返回
。因为特定的工做流程,使用者必须在建立Cache或者获取数据时指定不存在数据时应当怎么获取数据。GuavaCache采用LRU的工做原理,使用者必须指定缓存数据的大小,当超过缓存大小时,一定引起数据删除。GuavaCache还可让用户指定缓存数据的过时时间,刷新时间等等不少有用的功能。
a.CacheLoader
/** * CacheLoader 当检索不存在的时候,会自动的加载信息的! */ private static LoadingCache<String, String> loadingCache = CacheBuilder .newBuilder() .maximumSize(2) .expireAfterWrite(10, TimeUnit.SECONDS) .concurrencyLevel(2) .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(2) .expireAfterWrite(10, TimeUnit.SECONDS) .concurrencyLevel(2) .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; } }
使用构造器重载咱们须要定义不少构造器,为了应对使用者不一样的需求(有些可能只须要id,有些须要id和name,有些只须要name,......),理论上咱们须要定义2^4 = 16个构造器,这只是4个参数,若是参数更多的话,那将是指数级增加,确定是不合理的。要么你定义一个所有参数的构造器,使用者只能多传入一些不须要的属性值来匹配你的构造器。很明显这种构造器重载的方式对于多属性的状况是不完美的。 (问题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二者只能生效一个是不能同时使用的!
当 建立 或 写以后的 固定 有效期到达时,数据会被自动从缓存中移除,
2.expireAfterAccess
指明每一个数据实体:当 建立 或 写 或 读 以后的 固定值的有效期到达时,数据会被自动从缓存中移除。读写操做都会重置访问时间,但asMap方法不会。
3.refreshAfterWrite
指明每一个数据实体:当 建立 或 写 以后的 固定值的有效期到达时,且新请求过来时,数据会被自动刷新(注意不是删除是异步刷新,不会阻塞读取,先返回旧值,异步重载到数据返回后复写新值)。
数据过时不会自动重载,而是经过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 = 1; @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 - 1; AtomicReferenceArray<ReferenceEntry<K, V>> table = this.table; int index = hash & (table.length() - 1); 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(1); // 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(1); } } 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(2000); logger.info("LoadingCache测试 从mysql加载缓存成功"); Man tmpman = new Man(); tmpman.setId(key); tmpman.setName("其余人"); if (key.equals("001")) { tmpman.setName("张三"); return tmpman; } if (key.equals("002")) { tmpman.setName("李四"); return tmpman; } return tmpman; } }; //缓存数量为1,为了展现缓存删除效果 loadingCache = CacheBuilder.newBuilder().maximumSize(1).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("001"); System.out.println(man); System.out.println("\n使用loadingCache getIfPresent方法 第一次加载"); man = cachDemo.getIfPresentloadingCache("002"); System.out.println(man); System.out.println("\n使用loadingCache get方法 第一次加载"); man = cachDemo.getCacheKeyloadingCache("002"); System.out.println(man); System.out.println("\n使用loadingCache get方法 已加载过"); man = cachDemo.getCacheKeyloadingCache("002"); System.out.println(man); System.out.println("\n使用loadingCache get方法 已加载过,可是已经被剔除掉,验证从新加载"); man = cachDemo.getCacheKeyloadingCache("001"); System.out.println(man); System.out.println("\n使用loadingCache getIfPresent方法 已加载过"); man = cachDemo.getIfPresentloadingCache("001"); System.out.println(man); System.out.println("\n使用loadingCache put方法 再次get"); Man newMan = new Man(); newMan.setId("001"); newMan.setName("额外添加"); cachDemo.putloadingCache("001",newMan); man = cachDemo.getCacheKeyloadingCache("001"); 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(10, TimeUnit.SECONDS).maximumSize(500).build(); cache.get("key", new Callable<Object>() { //Callable 加载 @Override public Object call() throws Exception { return "value"; } }); LoadingCache<String, Object> loadingCache = CacheBuilder.newBuilder() .expireAfterAccess(30, TimeUnit.SECONDS).maximumSize(5) .build(new CacheLoader<String, Object>() { @Override public Object load(String key) throws Exception { return "value"; } });
若是有合理的默认方法来加载或计算与键关联的值。
LoadingCache是附带CacheLoader构建而成的缓存实现。建立本身的CacheLoader一般只须要简单地实现V load(K key) throws Exception方法。
从LoadingCache查询的正规方式是使用get(K)方法。这个方法要么返回已经缓存的值,要么使用CacheLoader向缓存原子地加载新值。因为CacheLoader可能抛出异常,LoadingCache.get(K)也声明为抛出ExecutionException异常。
若是没有合理的默认方法来加载或计算与键关联的值,或者想要覆盖默认的加载运算,同时保留“获取缓存-若是没有-则计算”[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("1234",obj1); obj1 = new String("123"); System.gc(); System.out.println(testCache.getIfPresent("1234")); System.out.println(testCache.stats()); } }
运行结果
缓存构建器。构建缓存的入口,指定缓存配置参数并初始化本地缓存。
主要采用builder的模式,CacheBuilder的每个方法都返回这个CacheBuilder知道build方法的调用。
注意build方法有重载,带有参数的为构建一个具备数据加载功能的缓存,不带参数的构建一个没有数据加载功能的缓存。
做为LocalCache的一个内部类,在构造方法里面会把LocalCache类型的变量传入,而且调用方法时都直接或者间接调用LocalCache里面的方法。
能够看到该类继承了LocalManualCache并实现接口LoadingCache。
覆盖了get,getUnchecked等方法。
上一篇文章讲了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,很好隐藏了细节,总结来讲
一、LocalManualCache实现了Cache,具备了全部cache方法。
二、LocalLoadingCache实现了LoadingCache,具备了全部LoadingCache方法。
三、LocalLoadingCache继承了LocalManualCache,那么对外暴露的LocalLoadingCache的方法既有自身须要的,又有cache应该具备的。
四、经过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();
五、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()); }
刷新和回收不太同样。正如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(1, TimeUnit.SECONDS).maximumSize(1000).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(1, TimeUnit.SECONDS).maximumSize(1000).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 = 4; while (times-- > 0) { Thread.sleep(900); 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(10).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(10)); 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(2); 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-2-thread-1 ========END======= STEP1 >>>pool-2-thread-2 STEP2 >>>pool-2-thread-1 BEGIN--->|||step 1 ===--===||| |||step 2 ===--===---||| =======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 = 1; 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 = 1; @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 = 16; // 默认的初始化Map大小 private static final int DEFAULT_CONCURRENCY_LEVEL = 4; // 默认并发水平 private static final int DEFAULT_EXPIRATION_NANOS = 0; // 默认超时 private static final int DEFAULT_REFRESH_NANOS = 0; // 默认刷新时间 static final int UNSET_INT = -1; 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(0, 0, 0, 0, 0, 0);// 初始状态的统计对象 /** * 系统实现的简单的缓存状态统计类 */ 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 1; } } static final Ticker NULL_TICKER = new Ticker() { @Override public long read() { return 0; } }; /** * 默认的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 = 0; int segmentCount = 1; while (segmentCount < concurrencyLevel && (!evictsBySize() || segmentCount * 20 <= maxWeight)) { ++segmentShift; segmentCount <<= 1; } this.segmentShift = 32 - segmentShift;//位 偏移数 segmentMask = segmentCount - 1;//mask码 this.segments = newSegmentArray(segmentCount);// 构造数据数组,如上图所示 //获取每一个segment初始化容量,而且保证大于等于map初始容量 int segmentCapacity = initialCapacity / segmentCount; if (segmentCapacity * segmentCount < initialCapacity) { ++segmentCapacity; } //段Size 必须为2的次数,而且刚刚大于段初始容量 int segmentSize = 1; while (segmentSize < segmentCapacity) { segmentSize <<= 1; } // 权重设置,确保权重和==map权重 if (evictsBySize()) { // Ensure sum of segment max weights = overall max weights long maxSegmentWeight = maxWeight / segmentCount + 1; long remainder = maxWeight % segmentCount; for (int i = 0; i < this.segments.length; ++i) { if (i == remainder) { maxSegmentWeight--; } //构造每一个段结构 this.segments[i] = createSegment(segmentSize, maxSegmentWeight, builder.getStatsCounterSupplier().get()); } } else { for (int i = 0; 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 = 0; i < segments.length; ++i) { if (segments[i].count != 0) { return false; } sum += segments[i].modCount; } if (sum != 0L) { // recheck unless no modifications for (int i = 0; i < segments.length; ++i) { if (segments[i].count != 0) { 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() * 3 / 4; // 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 为了知足并发场景的使用,核心的数据结构就是按照 ConcurrentHashMap 来的,这里也是一个 key 定位到一个具体位置的过程。
先找到 Segment,再找具体的位置,等因而作了两次 Hash 定位。
主要的类:
CacheBuilder 设置参数,构建Cache
LocalCache 是核心实现,虽然builder构建的是LocalLoadingCache(带refresh功能)和LocalManualCache(不带refresh功能),但其实那两个只是个壳子
提要:
记录所需参数
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之类的数据 }
在上文的分析中能够看出 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(2, 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(0); } });
执行结果:
12018-07-15 20:41:07.433 [main] INFO c.crossoverjie.guava.CacheLoaderTest - 当前缓存值=0,缓存大小=1 22018-07-15 20:41:07.442 [main] INFO c.crossoverjie.guava.CacheLoaderTest - 缓存的全部内容={1000=0} 32018-07-15 20:41:07.443 [main] INFO c.crossoverjie.guava.CacheLoaderTest - job running times=10 42018-07-15 20:41:10.461 [main] INFO c.crossoverjie.guava.CacheLoaderTest - 删除缘由=EXPIRED,删除 key=1000,删除 value=1 52018-07-15 20:41:10.462 [main] INFO c.crossoverjie.guava.CacheLoaderTest - 当前缓存值=0,缓存大小=1 62018-07-15 20:41:10.462 [main] INFO c.crossoverjie.guava.CacheLoaderTest - 缓存的全部内容={1000=0}
能够看出当缓存被删除的时候会回调咱们自定义的函数,并告知删除缘由。
那么 Guava 是如何实现的呢?

根据 LocalCache 中的 getLiveValue()
中判断缓存过时时,跟着这里的调用关系就会一直跟到:

removeValueFromChain()
中的:

enqueueNotification()
方法会将回收的缓存(包含了 key,value)以及回收缘由包装成以前定义的事件接口加入到一个本地队列中。

这样一看也没有回调咱们初始化时候的事件啊。
不过用过队列的同窗应该能猜出,既然这里写入队列,那就确定就有消费。
咱们回到获取缓存的地方:

在 finally 中执行了 postReadCleanup()
方法;其实在这里面就是对刚才的队列进行了消费:

一直跟进来就会发现这里消费了队列,将以前包装好的移除消息调用了咱们自定义的事件,这样就完成了一次事件回调。
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能够保证只让一个线程去加载数据(好比从数据库中),而其余线程则等待这个线程的返回结果。这样就能避免大量用户请求穿透缓存。
在日常开发过程当中,不少状况须要使用缓存来避免频繁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 对于其核心实现我会作以下的设计:
源码参考:guava源码
参考:Guava Cache特性:对于同一个key,只让一个请求回源load数据,其余线程阻塞等待结果