正文
一、引子
缓存有很多种解决方案,常见的是:
1.存储在内存中 : 内存缓存顾名思义直接存储在JVM内存中,JVM宕机那么内存丢失,读写速度快,但受内存大小的限制,且有丢失数据风险。
2.存储在磁盘中: 即从内存落地并序列化写入磁盘的缓存,持久化在磁盘,读写需要IO效率低,但是安全。
3.内存+磁盘组合方式:这种组合模式有很多成熟缓存组件,也是高效且安全的策略,比如redis。
本文分析常用的内存缓存:google cache。源码包:com.google.guava:guava:22.0 jar包下的pcom.google.common.cache包,适用于高并发读写场景,可自定义缓存失效策略。
二、使用方法
2.1 CacheBuilder有3种失效重载模式
1.expireAfterWrite
当 创建 或 写之后的 固定 有效期到达时,数据会被自动从缓存中移除,源码注释如下:
1 /**指明每个数据实体:当 创建 或 最新一次更新 之后的 固定值的 有效期到达时,数据会被自动从缓存中移除 2 * Specifies that each entry should be automatically removed from the cache once a fixed duration 3 * has elapsed after the entry's creation, or the most recent replacement of its value. 4 *当间隔被设置为0时,maximumSize设置为0,忽略其它容量和权重的设置。这使得测试时 临时性地 禁用缓存且不用改代码。 5 *When {@code duration} is zero, this method hands off to {@link #maximumSize(long) 6 * maximumSize}{@code (0)}, ignoring any otherwise-specified maximum size or weight. This can be 7 * useful in testing, or to disable caching temporarily without a code change. 8 *过期的数据实体可能会被Cache.size统计到,但不能进行读写,数据过期后会被清除。 9 *
Expired entries may be counted in {@link Cache#size}, but will never be visible to read or 10 * write operations. Expired entries are cleaned up as part of the routine maintenance described 11 * in the class javadoc. 12 * 13 * @param duration the length of time after an entry is created that it should be automatically 14 * removed 15 * @param unit the unit that {@code duration} is expressed in 16 * @return this {@code CacheBuilder} instance (for chaining) 17 * @throws IllegalArgumentException if {@code duration} is negative 18 * @throws IllegalStateException if the time to live or time to idle was already set 19 */ 20 public CacheBuilder
expireAfterWrite(long duration, TimeUnit unit) { 21 checkState( 22 expireAfterWriteNanos == UNSET_INT, 23 "expireAfterWrite was already set to %s ns", 24 expireAfterWriteNanos); 25 checkArgument(duration >= 0, "duration cannot be negative: %s %s", duration, unit); 26 this.expireAfterWriteNanos = unit.toNanos(duration); 27 return this; 28 }
2.expireAfterAccess
指明每个数据实体:当 创建 或 写 或 读 之后的 固定值的有效期到达时,数据会被自动从缓存中移除。读写操作都会重置访问时间,但asMap方法不会。源码注释如下:
1 /**指明每个数据实体:当 创建 或 更新 或 访问 之后的 固定值的有效期到达时,数据会被自动从缓存中移除。读写操作都会重置访问时间,但asMap方法不会。 2 * Specifies that each entry should be automatically removed from the cache once a fixed duration 3 * has elapsed after the entry's creation, the most recent replacement of its value, or its last 4 * access. Access time is reset by all cache read and write operations (including 5 * {@code Cache.asMap().get(Object)} and {@code Cache.asMap().put(K, V)}), but not by operations 6 * on the collection-views of {@link Cache#asMap}. 7 * 后面的同expireAfterWrite 8 *When {@code duration} is zero, this method hands off to {@link #maximumSize(long) 9 * maximumSize}{@code (0)}, ignoring any otherwise-specified maximum size or weight. This can be 10 * useful in testing, or to disable caching temporarily without a code change. 11 * 12 *
Expired entries may be counted in {@link Cache#size}, but will never be visible to read or 13 * write operations. Expired entries are cleaned up as part of the routine maintenance described 14 * in the class javadoc. 15 * 16 * @param duration the length of time after an entry is last accessed that it should be 17 * automatically removed 18 * @param unit the unit that {@code duration} is expressed in 19 * @return this {@code CacheBuilder} instance (for chaining) 20 * @throws IllegalArgumentException if {@code duration} is negative 21 * @throws IllegalStateException if the time to idle or time to live was already set 22 */ 23 public CacheBuilder
expireAfterAccess(long duration, TimeUnit unit) { 24 checkState( 25 expireAfterAccessNanos == UNSET_INT, 26 "expireAfterAccess was already set to %s ns", 27 expireAfterAccessNanos); 28 checkArgument(duration >= 0, "duration cannot be negative: %s %s", duration, unit); 29 this.expireAfterAccessNanos = unit.toNanos(duration); 30 return this; 31 }
3.refreshAfterWrite
指明每个数据实体:当 创建 或 写 之后的 固定值的有效期到达时,数据会被自动刷新(注意不是删除是异步刷新,不会阻塞读取,先返回旧值,异步重载到数据返回后复写新值)。源码注释如下:
1 /**指明每个数据实体:当 创建 或 更新 之后的 固定值的有效期到达时,数据会被自动刷新。刷新方法在LoadingCache接口的refresh()申明,实际最终调用的是CacheLoader的reload() 2 * Specifies that active entries are eligible for automatic refresh once a fixed duration has 3 * elapsed after the entry's creation, or the most recent replacement of its value. The semantics 4 * of refreshes are specified in {@link LoadingCache#refresh}, and are performed by calling 5 * {@link CacheLoader#reload}. 6 * 默认reload是同步方法,所以建议用户覆盖reload方法,否则刷新将在无关的读写操作间操作。 7 *As the default implementation of {@link CacheLoader#reload} is synchronous, it is 8 * recommended that users of this method override {@link CacheLoader#reload} with an asynchronous 9 * implementation; otherwise refreshes will be performed during unrelated cache read and write 10 * operations. 11 * 12 *
Currently automatic refreshes are performed when the first stale request for an entry 13 * occurs. The request triggering refresh will make a blocking call to {@link CacheLoader#reload} 14 * and immediately return the new value if the returned future is complete, and the old value 15 * otherwise.触发刷新操作的请求会阻塞调用reload方法并且当返回的Future完成时立即返回新值,否则返回旧值。 16 * 17 *
Note: all exceptions thrown during refresh will be logged and then swallowed. 18 * 19 * @param duration the length of time after an entry is created that it should be considered 20 * stale, and thus eligible for refresh 21 * @param unit the unit that {@code duration} is expressed in 22 * @return this {@code CacheBuilder} instance (for chaining) 23 * @throws IllegalArgumentException if {@code duration} is negative 24 * @throws IllegalStateException if the refresh interval was already set 25 * @since 11.0 26 */ 27 @GwtIncompatible // To be supported (synchronously). 28 public CacheBuilder
refreshAfterWrite(long duration, TimeUnit unit) { 29 checkNotNull(unit); 30 checkState(refreshNanos == UNSET_INT, "refresh was already set to %s ns", refreshNanos); 31 checkArgument(duration > 0, "duration must be positive: %s %s", duration, unit); 32 this.refreshNanos = unit.toNanos(duration); 33 return this; 34 }
2.2 测试验证
1)定义一个静态的LoadingCache,用cacheBuilder构造缓存,分别定义了同步load(耗时2秒)和异步reload(耗时2秒)方法。
2)在main方法中,往缓存中设置值,定义3个线程,用CountDownLatch倒计时器模拟3个线程并发读取缓存,最后在主线程分别5秒、0.5秒、2秒时get缓存。
测试代码如下:
1 package guava; 2 3 import com.google.common.cache.CacheBuilder; 4 import com.google.common.cache.CacheLoader; 5 import com.google.common.cache.LoadingCache; 6 import com.google.common.util.concurrent.ListenableFuture; 7 import com.google.common.util.concurrent.ListeningExecutorService; 8 import com.google.common.util.concurrent.MoreExecutors; 9 10 import java.util.Date; 11 import java.util.Random; 12 import java.util.concurrent.Callable; 13 import java.util.concurrent.CountDownLatch; 14 import java.util.concurrent.Executors; 15 import java.util.concurrent.TimeUnit; 16 17 /** 18 * @ClassName guava.LoadingCacheTest 19 * @Description 注意refresh并不会主动刷新,而是被检索触发更新value,且随时可返回旧值 20 * @Author denny 21 * @Date 2018/4/28 下午12:10 22 */ 23 public class LoadingCacheTest { 24 25 // guava线程池,用来产生ListenableFuture 26 private static ListeningExecutorService service = MoreExecutors.listeningDecorator( 27 Executors.newFixedThreadPool(10)); 28 29 /** 30 * 1.expireAfterWrite:指定时间内没有创建/覆盖时,会移除该key,下次取的时候触发"同步load"(一个线程执行load) 31 * 2.refreshAfterWrite:指定时间内没有被创建/覆盖,则指定时间过后,再次访问时,会去刷新该缓存,在新值没有到来之前,始终返回旧值 32 * "异步reload"(也是一个线程执行reload) 33 * 3.expireAfterAccess:指定时间内没有读写,会移除该key,下次取的时候从loading中取 34 * 区别:指定时间过后,expire是remove该key,下次访问是同步去获取返回新值; 35 * 而refresh则是指定时间后,不会remove该key,下次访问会触发刷新,新值没有回来时返回旧值 36 * 37 * 同时使用:可避免定时刷新+定时删除下次访问载入 38 */ 39 private static final LoadingCachecache = CacheBuilder.newBuilder() 40 .maximumSize(1000) 41 //.refreshAfterWrite(1, TimeUnit.SECONDS) 42 .expireAfterWrite(1, TimeUnit.SECONDS) 43 //.expireAfterAccess(1,TimeUnit.SECONDS) 44 .build(new CacheLoader () { 45 @Override 46 public String load(String key) throws Exception { 47 System.out.println(Thread.currentThread().getName() +"==load start=="+",时间=" + new Date()); 48 // 模拟同步重载耗时2秒 49 Thread.sleep(2000); 50 String value = "load-" + new Random().nextInt(10); 51 System.out.println( 52 Thread.currentThread().getName() + "==load end==同步耗时2秒重载数据-key=" + key + ",value="+value+",时间=" + new Date()); 53 return value; 54 } 55 56 @Override 57 public ListenableFuture reload(final String key, final String oldValue) 58 throws Exception { 59 System.out.println( 60 Thread.currentThread().getName() + "==reload ==异步重载-key=" + key + ",时间=" + new Date()); 61 return service.submit(new Callable () { 62 @Override 63 public String call() throws Exception { 64 /* 模拟异步重载耗时2秒 */ 65 Thread.sleep(2000); 66 String value = "reload-" + new Random().nextInt(10); 67 System.out.println(Thread.currentThread().getName() + "==reload-callable-result="+value+ ",时间=" + new Date()); 68 return value; 69 } 70 }); 71 } 72 }); 73 74 //倒计时器 75 private static CountDownLatch latch = new CountDownLatch(1); 76 77 public static void main(String[] args) throws Exception { 78 79 System.out.println("启动-设置缓存" + ",时间=" + new Date()); 80 cache.put("name", "张三"); 81 System.out.println("缓存是否存在=" + cache.getIfPresent("name")); 82 //休眠 83 Thread.sleep(2000); 84 //System.out.println("2秒后"+",时间="+new Date()); 85 System.out.println("2秒后,缓存是否存在=" + cache.getIfPresent("name")); 86 //启动3个线程 87 for (int i = 0; i < 3; i++) { 88 startThread(i); 89 } 90 91 // -1直接=0,唤醒所有线程读取缓存,模拟并发访问缓存 92 latch.countDown(); 93 //模拟串行读缓存 94 Thread.sleep(5000); 95 System.out.println(Thread.currentThread().getName() + "休眠5秒后,读缓存="+cache.get("name")+",时间=" + new Date()); 96 Thread.sleep(500); 97 System.out.println(Thread.currentThread().getName() + "距离上一次读0.5秒后,读缓存="+cache.get("name")+",时间=" + new Date()); 98 Thread.sleep(2000); 99 System.out.println(Thread.currentThread().getName() + "距离上一次读2秒后,读缓存="+cache.get("name")+",时间=" + new Date()); 100 } 101 102 private static void startThread(int id) { 103 Thread t = new Thread(new Runnable() { 104 @Override 105 public void run() { 106 try { 107 System.out.println(Thread.currentThread().getName() + "...begin" + ",时间=" + new Date()); 108 //休眠,当倒计时器=0时唤醒线程 109 latch.await(); 110 //读缓存 111 System.out.println( 112 Thread.currentThread().getName() + "并发读缓存=" + cache.get("name") + ",时间=" + new Date()); 113 } catch (Exception e) { 114 e.printStackTrace(); 115 } 116 } 117 }); 118 119 t.setName("Thread-" + id); 120 t.start(); 121 } 122 }
结果分析
1.expireAfterWrite:当 创建 或 写 之后的 有效期到达时,数据会被自动从缓存中移除
启动-设置缓存,时间=Thu May 17 17:55:36 CST 2018-->主线程启动,缓存创建完毕并设值,即触发写缓存 缓存是否存在=张三 2秒后,缓存是否存在=null--》设定了1秒自动删除缓存,2秒后缓存不存在 Thread-0...begin,时间=Thu May 17 17:55:38 CST 2018--》38秒时,启动3个线程模拟并发读:三个线程读缓存,由于缓存不存在,阻塞在get方法上,等待其中一个线程去同步load数据 Thread-1...begin,时间=Thu May 17 17:55:38 CST 2018 Thread-2...begin,时间=Thu May 17 17:55:38 CST 2018 Thread-1==load start==,时间=Thu May 17 17:55:38 CST 2018---线程1,同步载入数据load() Thread-1==load end==同步耗时2秒重载数据-key=name,value=load-2,时间=Thu May 17 17:55:40 CST 2018--线程1,同步载入数据load()完毕!,即40秒时写入数据:load-2 Thread-0并发读缓存=load-2,时间=Thu May 17 17:55:40 CST 2018---线程1同步载入数据load()完毕后,3个阻塞在get方法的线程得到缓存值:load-2 Thread-1并发读缓存=load-2,时间=Thu May 17 17:55:40 CST 2018 Thread-2并发读缓存=load-2,时间=Thu May 17 17:55:40 CST 2018 main==load start==,时间=Thu May 17 17:55:43 CST 2018---主线程访问缓存不存在,执行load() main==load end==同步耗时2秒重载数据-key=name,value=load-4,时间=Thu May 17 17:55:45 CST 2018---load()完毕!45秒时写入数据:load-4 main休眠5秒后,读缓存=load-4,时间=Thu May 17 17:55:45 CST 2018---主线程得到缓存:load-4 main距离上一次读0.5秒后,读缓存=load-4,时间=Thu May 17 17:55:45 CST 2018--距离上一次写才0.5秒,数据有效:load-4 main==load start==,时间=Thu May 17 17:55:47 CST 2018-47秒时,距离上一次写45秒,超过了1秒,数据无效,再次load() main==load end==同步耗时2秒重载数据-key=name,value=load-8,时间=Thu May 17 17:55:49 CST 2018--49秒时load()完毕:load-8 main距离上一次读2秒后,读缓存=load-8,时间=Thu May 17 17:55:49 CST 2018--打印get的缓存结果:load-8
2.expireAfterAccess:当 创建 或 写 或 读 之后的 有效期到达时,数据会被自动从缓存中移除
修改测试代码98、99行:
Thread.sleep(700);
System.out.println(Thread.currentThread().getName() + "距离上一次读0.5秒后,读缓存="+cache.get("name")+",时间=" + new Date());
启动-设置缓存,时间=Thu May 17 18:32:38 CST 2018
缓存是否存在=张三
2秒后,缓存是否存在=null
Thread-0...begin,时间=Thu May 17 18:32:40 CST 2018
Thread-1...begin,时间=Thu May 17 18:32:40 CST 2018
Thread-2...begin,时间=Thu May 17 18:32:40 CST 2018
Thread-2==load start==,时间=Thu May 17 18:32:40 CST 2018
Thread-2==load end==同步耗时2秒重载数据-key=name,value=load-6,时间=Thu May 17 18:32:42 CST 2018
Thread-0并发读缓存=load-6,时间=Thu May 17 18:32:42 CST 2018
Thread-1并发读缓存=load-6,时间=Thu May 17 18:32:42 CST 2018
Thread-2并发读缓存=load-6,时间=Thu May 17 18:32:42 CST 2018
main==load start==,时间=Thu May 17 18:32:45 CST 2018
main==load end==同步耗时2秒重载数据-key=name,value=load-7,时间=Thu May 17 18:32:47 CST 2018----47秒时写
main休眠5秒后,读缓存=load-7,时间=Thu May 17 18:32:47 CST 2018
main距离上一次读0.5秒后,读缓存=load-7,时间=Thu May 17 18:32:48 CST 2018---48秒读
main距离上一次读0.5秒后,读缓存=load-7,时间=Thu May 17 18:32:49 CST 2018--49秒距离上一次写47秒,间距大于2秒,但是没有触发load() ,因为48秒时又读了一次,刷新了缓存有效期
3.refreshAfterWrite:当 创建 或 写 之后的 有效期到达时,数据会被自动刷新(注意不是删除是刷新)。
启动-设置缓存,时间=Thu May 17 18:39:59 CST 2018--》59秒写 缓存是否存在=张三 main==reload ==异步重载-key=name,时间=Thu May 17 18:40:01 CST 2018--》01秒,2秒后距离上次写超过1秒,reload异步重载 2秒后,缓存是否存在=张三--》距离上一次写过了2秒,但是会立即返回缓存 Thread-0...begin,时间=Thu May 17 18:40:01 CST 2018--》01秒3个线程并发访问 Thread-1...begin,时间=Thu May 17 18:40:01 CST 2018 Thread-2...begin,时间=Thu May 17 18:40:01 CST 2018 Thread-2并发读缓存=张三,时间=Thu May 17 18:40:01 CST 2018--》01秒3个线程都立即得到了缓存 Thread-0并发读缓存=张三,时间=Thu May 17 18:40:01 CST 2018 Thread-1并发读缓存=张三,时间=Thu May 17 18:40:01 CST 2018 pool-1-thread-1==reload-callable-result=reload-5,时间=Thu May 17 18:40:03 CST 2018--》01秒时的异步,2秒后也就是03秒时,查询结果:reload-5 main==reload ==异步重载-key=name,时间=Thu May 17 18:40:06 CST 2018--》06秒时,距离上一次写时间超过1秒,reload异步重载 main休眠5秒后,读缓存=reload-5,时间=Thu May 17 18:40:06 CST 2018--》06秒时,reload异步重载,立即返回旧值reload-5 main距离上一次读0.5秒后,读缓存=reload-5,时间=Thu May 17 18:40:07 CST 2018 main距离上一次读0.5秒后,读缓存=reload-5,时间=Thu May 17 18:40:07 CST 2018 pool-1-thread-2==reload-callable-result=reload-4,时间=Thu May 17 18:40:08 CST 2018--》06秒时的异步重载,2秒后也就是08秒,查询结果:reload-4
三、源码剖析
前面一节简单演示了google cache的几种用法,本节细看源码。
3.1 简介
我们就从构造器CacheBuilder的源码注释,来看一下google cache的简单介绍:
//LoadingCache加载缓存和缓存实例是以下的特性的组合:
1 A builder of LoadingCache and Cache instances having any combination of the following features: 2 automatic loading of entries into the cache-》把数据实体自动载入到缓存中去-》基本特性 3 least-recently-used eviction when a maximum size is exceeded-》当缓存到达最大数量时回收最少使用的数据-》限制最大内存,避免内存被占满-》高级特性,赞? 4 time-based expiration of entries, measured since last access or last write-》基于时间的实体有效期,依据最后访问或写时间-》基本特性,但很细腻 5 keys automatically wrapped in weak references-》缓存的keys自动用,弱引用封装-》利于GC回收-》赞? 6 values automatically wrapped in weak or soft references-》缓存的值values自动封装在弱引用或者软引用中-》赞? 7 notification of evicted (or otherwise removed) entries-》回收或被移除实体可收到通知-》赞? 8 accumulation of cache access statistics--》缓存的访问统计-》赞?
9 These features are all optional; caches can be created using all or none of them. By default cache instances created by CacheBuilder will not perform any type of eviction. 10 Usage example://使用样例: 11 12 LoadingCachegraphs = CacheBuilder.newBuilder() 13 .maximumSize(10000) 14 .expireAfterWrite(10, TimeUnit.MINUTES) 15 .removalListener(MY_LISTENER) 16 .build( 17 new CacheLoader () { 18 public Graph load(Key key) throws AnyException { 19 return createExpensiveGraph(key); 20 } 21 }); 22 Or equivalently,//等同于 23 24 // In real life this would come from a command-line flag or config file 支持字符串载入数据 25 String spec = "maximumSize=10000,expireAfterWrite=10m"; 26 27 LoadingCache graphs = CacheBuilder.from(spec) 28 .removalListener(MY_LISTENER) 29 .build( 30 new CacheLoader () { 31 public Graph load(Key key) throws AnyException { 32 return createExpensiveGraph(key); 33 } 34 });
//这个缓存被实现成一个类似ConcurrentHashMap高性能的哈希表。是线程安全的,但是其它线程并发修改了这个缓存,会显示在迭代器访问中,但是不会报ConcurrentModificationException错。 35 The returned cache is implemented as a hash table with similar performance characteristics to ConcurrentHashMap. It implements all optional operations of the LoadingCache and Cache interfaces.
The asMap view (and its collection views) have weakly consistent iterators. This means that they are safe for concurrent use, but if other threads modify the cache after the iterator is created,
it is undefined which of these changes, if any, are reflected in that iterator. These iterators never throw ConcurrentModificationException.
//默认使用equals方法(内容相同)判断key/value的相等,但如果申明了弱引用key 或者 弱/软引用的value,那么必须使用==判断相等(内存地址相同) 36 Note: by default, the returned cache uses equality comparisons (the equals method) to determine equality for keys or values. However, if weakKeys() was specified, the cache uses identity (==) comparisons instead for keys. Likewise, if weakValues() or softValues() was specified, the cache uses identity comparisons for values. 37
//很多种情况会导致缓存的数据被剔除
Entries are automatically evicted from the cache when any of maximumSize, maximumWeight, expireAfterWrite, expireAfterAccess, weakKeys, weakValues, or softValues are requested.
38 If maximumSize or maximumWeight is requested entries may be evicted on each cache modification.
//写后失效或者访问后失效,实体可能在每个缓存修改时被剔除,Cache.size()可能会被统计到,但肯定是无法访问到。 39 If expireAfterWrite or expireAfterAccess is requested entries may be evicted on each cache modification, on occasional cache accesses, or on calls to Cache.cleanUp(). Expired entries may be counted by Cache.size(), but will never be visible to read or write operations. 40 If weakKeys, weakValues, or softValues are requested, it is possible for a key or value present in the cache to be reclaimed by the garbage collector. Entries with reclaimed keys or values may be removed from the cache on each cache modification, on occasional cache accesses, or on calls to Cache.cleanUp(); such entries may be counted in Cache.size(), but will never be visible to read or write operations. 41
//这里不用管了...
Certain cache configurations will result in the accrual of periodic maintenance tasks which will be performed during write operations, or during occasional read operations in the absence of writes. The Cache.cleanUp() method of the returned cache will also perform maintenance, but calling it should not be necessary with a high throughput cache. Only caches built with removalListener, expireAfterWrite, expireAfterAccess, weakKeys, weakValues, or softValues perform periodic maintenance. 42 The caches produced by CacheBuilder are serializable, and the deserialized caches retain all the configuration properties of the original cache. Note that the serialized form does not include cache contents, but only configuration.
如上图所示,我们知道两点:
1.特性
- 把数据实体自动载入到缓存中去-》基本特性
- 当缓存到达最大数量时回收最少使用的数据-》限制最大内存,避免内存被占满-》高级特性,赞?
- 基于时间的实体有效期,依据最后访问或写时间-》基本特性,但很细腻
- 缓存的keys自动用,弱引用封装-》利于GC回收-》赞?
- 回收或被移除实体可收到通知-》赞?
- 缓存的访问统计-》赞?
2.数据结构
类似ConcurrentHashMap高性能的哈希表。是线程安全的,
3.2 源码剖析
从上节简介中我们可以找到几个需要深度剖析的点:
- 数据结构
- 构造器
- 数据过期重载
- 缓存回收机制
1.数据结构
先看一下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类型)的相应方法。
先来看一张LocalCache的数据结构图:
如上图所示:LocalCache类似ConcurrentHashMap采用了分段策略,通过减小锁的粒度来提高并发,LocalCache中数据存储在Segment[]中,每个segment又包含5个队列和一个table,这个table是自定义的一种类数组的结构,每个元素都包含一个ReferenceEntry
这些队列,前2个是key、value引用队列用以加速GC回收,后3个队列记录用户的写记录、访问记录、高频访问顺序队列用以实现LRU算法。AtomicReferenceArray是JUC包下的Doug Lea老李头设计的类:一组对象引用,其中元素支持原子性更新。
最后是ReferenceEntry:引用数据存储接口,默认强引用,类图如下:
2.CacheBuilder构造器
1 private static final LoadingCachecache = CacheBuilder.newBuilder() 2 .maximumSize(1000) 3 .refreshAfterWrite(1, TimeUnit.SECONDS) 4 //.expireAfterWrite(1, TimeUnit.SECONDS) 5 //.expireAfterAccess(1,TimeUnit.SECONDS) 6 .build(new CacheLoader () { 7 @Override 8 public String load(String key) throws Exception { 9 System.out.println(Thread.currentThread().getName() +"==load start=="+",时间=" + new Date()); 10 // 模拟同步重载耗时2秒 11 Thread.sleep(2000); 12 String value = "load-" + new Random().nextInt(10); 13 System.out.println( 14 Thread.currentThread().getName() + "==load end==同步耗时2秒重载数据-key=" + key + ",value="+value+",时间=" + new Date()); 15 return value; 16 } 17 18 @Override 19 public ListenableFuture reload(final String key, final String oldValue) 20 throws Exception { 21 System.out.println( 22 Thread.currentThread().getName() + "==reload ==异步重载-key=" + key + ",时间=" + new Date()); 23 return service.submit(new Callable () { 24 @Override 25 public String call() throws Exception { 26 /* 模拟异步重载耗时2秒 */ 27 Thread.sleep(2000); 28 String value = "reload-" + new Random().nextInt(10); 29 System.out.println(Thread.currentThread().getName() + "==reload-callable-result="+value+ ",时间=" + new Date()); 30 return value; 31 } 32 }); 33 } 34 });
如上图所示:CacheBuilder参数设置完毕后最后调用build(CacheLoader )构造,参数是用户自定义的CacheLoader缓存加载器,复写一些方法(load,reload),返回LoadingCache接口(一种面向接口编程的思想,实际返回具体实现类)如下图:
1 publicLoadingCache build( 2 CacheLoader super K1, V1> loader) { 3 checkWeightWithWeigher(); 4 return new LocalCache.LocalLoadingCache (this, loader); 5 }
实际是构造了一个LoadingCache接口的实现类:LocalCache的静态类LocalLoadingCache,本地加载缓存类。
1 LocalLoadingCache( 2 CacheBuilder super K, ? super V> builder, CacheLoader super K, V> loader) { 3 super(new LocalCache(builder, checkNotNull(loader)));//LocalLoadingCache构造函数需要一个LocalCache作为参数 4 } 5 //构造LocalCache 6 LocalCache( 7 CacheBuilder super K, ? super V> builder, @Nullable CacheLoader super K, V> loader) { 8 concurrencyLevel = Math.min(builder.getConcurrencyLevel(), MAX_SEGMENTS);//默认并发水平是4 9 10 keyStrength = builder.getKeyStrength();//key的强引用 11 valueStrength = builder.getValueStrength(); 12 13 keyEquivalence = builder.getKeyEquivalence();//key比较器 14 valueEquivalence = builder.getValueEquivalence(); 15 16 maxWeight = builder.getMaximumWeight(); 17 weigher = builder.getWeigher(); 18 expireAfterAccessNanos = builder.getExpireAfterAccessNanos();//读写后有效期,超时重载 19 expireAfterWriteNanos = builder.getExpireAfterWriteNanos();//写后有效期,超时重载 20 refreshNanos = builder.getRefreshNanos(); 21 22 removalListener = builder.getRemovalListener();//缓存触发失效 或者 GC回收软/弱引用,触发监听器 23 removalNotificationQueue =//移除通知队列 24 (removalListener == NullListener.INSTANCE) 25 ? LocalCache. >discardingQueue() 26 : new ConcurrentLinkedQueue >(); 27 28 ticker = builder.getTicker(recordsTime()); 29 entryFactory = EntryFactory.getFactory(keyStrength, usesAccessEntries(), usesWriteEntries()); 30 globalStatsCounter = builder.getStatsCounterSupplier().get(); 31 defaultLoader = loader;//缓存加载器 32 33 int initialCapacity = Math.min(builder.getInitialCapacity(), MAXIMUM_CAPACITY); 34 if (evictsBySize() && !customWeigher()) { 35 initialCapacity = Math.min(initialCapacity, (int) maxWeight); 36 }
3.数据过期重载
数据过期不会自动重载,而是通过get操作时执行过期重载。具体就是上面追踪到了CacheBuilder构造的LocalLoadingCache,类图如下:
返回LocalCache.LocalLoadingCache后
就可以调用如下方法:
1 static class LocalLoadingCacheextends LocalManualCache 2 implements LoadingCache { 3 4 LocalLoadingCache( 5 CacheBuilder super K, ? super V> builder, CacheLoader super K, V> loader) { 6 super(new LocalCache (builder, checkNotNull(loader))); 7 } 8 9 // LoadingCache methods 10 11 @Override 12 public V get(K key) throws ExecutionException { 13 return localCache.getOrLoad(key); 14 } 15 16 @Override 17 public V getUnchecked(K key) { 18 try { 19 return get(key); 20 } catch (ExecutionException e) { 21 throw new UncheckedExecutionException(e.getCause()); 22 } 23 } 24 25 @Override 26 public ImmutableMap getAll(Iterable extends K> keys) throws ExecutionException { 27 return localCache.getAll(keys); 28 } 29 30 @Override 31 public void refresh(K key) { 32 localCache.refresh(key); 33 } 34 35 @Override 36 public final V apply(K key) { 37 return getUnchecked(key); 38 } 39 40 // Serialization Support 41 42 private static final long serialVersionUID = 1; 43 44 @Override 45 Object writeReplace() { 46 return new LoadingSerializationProxy (localCache); 47 } 48 }
最终get方法
@Override public V get(K key) throws ExecutionException { return localCache.getOrLoad(key); } V get(K key, CacheLoader super K, V> loader) throws ExecutionException { int hash = hash(checkNotNull(key));--》计算key的哈希值 return segmentFor(hash).get(key, hash, loader);--》先根据哈希值找到segment,再get返回value } SegmentsegmentFor(int hash) { // TODO(fry): Lazily create segments? return segments[(hash >>> segmentShift) & segmentMask]; } V get(K key, int hash, CacheLoader super K, V> loader) throws ExecutionException { checkNotNull(key); checkNotNull(loader); try { if (count != 0) { // 读volatile 当前段的元素个数,如果存在元素 // don't call getLiveEntry, which would ignore loading values ReferenceEntry e = getEntry(key, hash); if (e != null) { long now = map.ticker.read(); V value = getLiveValue(e, now); if (value != null) { recordRead(e, now);//记录访问时间,并添加进最近使用(LRU)队列 statsCounter.recordHits(1);//命中缓存,基数+1 return scheduleRefresh(e, key, hash, value, now, loader);//刷新值并返回 } ValueReference valueReference = e.getValueReference(); if (valueReference.isLoading()) {//如果正在重载数据,等待重载完毕后返回值 return waitForLoadingValue(e, key, valueReference); } } } // 当前segment中找不到实体 return lockedGetOrLoad(key, hash, loader); } catch (ExecutionException ee) { Throwable cause = ee.getCause(); if (cause instanceof Error) { throw new ExecutionError((Error) cause); } else if (cause instanceof RuntimeException) { throw new UncheckedExecutionException(cause); } throw ee; } finally { postReadCleanup(); } }
刷新:
1 V scheduleRefresh( 2 ReferenceEntryentry, 3 K key, 4 int hash, 5 V oldValue, 6 long now, 7 CacheLoader super K, V> loader) { 8 if (map.refreshes() 9 && (now - entry.getWriteTime() > map.refreshNanos) 10 && !entry.getValueReference().isLoading()) { 11 V newValue = refresh(key, hash, loader, true);//重载数据 12 if (newValue != null) {//重载数据成功,直接返回 13 return newValue; 14 } 15 }//否则返回旧值 16 return oldValue; 17 }
刷新核心方法:
1 V refresh(K key, int hash, CacheLoader super K, V> loader, boolean checkTime) { 2 final LoadingValueReferenceloadingValueReference = 3 insertLoadingValueReference(key, hash, checkTime); 4 if (loadingValueReference == null) { 5 return null; 6 } 7 //异步重载数据 8 ListenableFuture result = loadAsync(key, hash, loadingValueReference, loader); 9 if (result.isDone()) { 10 try { 11 return Uninterruptibles.getUninterruptibly(result); 12 } catch (Throwable t) { 13 // don't let refresh exceptions propagate; error was already logged 14 } 15 } 16 return null; 17 } 18 19 ListenableFuture loadAsync( 20 final K key, 21 final int hash, 22 final LoadingValueReference loadingValueReference, 23 CacheLoader super K, V> loader) { 24 final ListenableFuture loadingFuture = loadingValueReference.loadFuture(key, loader); 25 loadingFuture.addListener( 26 new Runnable() { 27 @Override 28 public void run() { 29 try { 30 getAndRecordStats(key, hash, loadingValueReference, loadingFuture); 31 } catch (Throwable t) { 32 logger.log(Level.WARNING, "Exception thrown during refresh", t); 33 loadingValueReference.setException(t); 34 } 35 } 36 }, 37 directExecutor()); 38 return loadingFuture; 39 } 40 41 public ListenableFuture loadFuture(K key, CacheLoader super K, V> loader) { 42 try { 43 stopwatch.start(); 44 V previousValue = oldValue.get(); 45 if (previousValue == null) { 46 V newValue = loader.load(key); 47 return set(newValue) ? futureValue : Futures.immediateFuture(newValue); 48 } 49 ListenableFuture newValue = loader.reload(key, previousValue); 50 if (newValue == null) { 51 return Futures.immediateFuture(null); 52 } 53 // To avoid a race, make sure the refreshed value is set into loadingValueReference 54 // *before* returning newValue from the cache query. 55 return transform( 56 newValue, 57 new com.google.common.base.Function () { 58 @Override 59 public V apply(V newValue) { 60 LoadingValueReference.this.set(newValue); 61 return newValue; 62 } 63 }, 64 directExecutor()); 65 } catch (Throwable t) { 66 ListenableFuture result = setException(t) ? futureValue : fullyFailedFuture(t); 67 if (t instanceof InterruptedException) { 68 Thread.currentThread().interrupt(); 69 } 70 return result; 71 } 72 }
如上图,最终刷新调用的是CacheBuilder中预先设置好的CacheLoader接口实现类的reload方法实现的异步刷新。
返回get主方法,如果当前segment中找不到key对应的实体,同步阻塞重载数据:
1 V lockedGetOrLoad(K key, int hash, CacheLoader super K, V> loader) throws ExecutionException { 2 ReferenceEntrye; 3 ValueReference valueReference = null; 4 LoadingValueReference loadingValueReference = null; 5 boolean createNewEntry = true; 6 7 lock(); 8 try { 9 // re-read ticker once inside the lock 10 long now = map.ticker.read(); 11 preWriteCleanup(now); 12 13 int newCount = this.count - 1; 14 AtomicReferenceArray > table = this.table; 15 int index = hash & (table.length() - 1); 16 ReferenceEntry first = table.get(index); 17 18 for (e = first; e != null; e = e.getNext()) { 19 K entryKey = e.getKey(); 20 if (e.getHash() == hash 21 && entryKey != null 22 && map.keyEquivalence.equivalent(key, entryKey)) { 23 valueReference = e.getValueReference(); 24 if (valueReference.isLoading()) {//如果正在重载,那么不需要重新再新建实体对象 25 createNewEntry = false; 26 } else { 27 V value = valueReference.get(); 28 if (value == null) {//如果被GC回收,添加进移除队列,等待remove监听器执行 29 enqueueNotification( 30 entryKey, hash, value, valueReference.getWeight(), RemovalCause.COLLECTED); 31 } else if (map.isExpired(e, now)) {//如果缓存过期,添加进移除队列,等待remove监听器执行 32 // This is a duplicate check, as preWriteCleanup already purged expired 33 // entries, but let's accomodate an incorrect expiration queue. 34 enqueueNotification( 35 entryKey, hash, value, valueReference.getWeight(), RemovalCause.EXPIRED); 36 } else {//不在重载,直接返回value 37 recordLockedRead(e, now); 38 statsCounter.recordHits(1); 39 // we were concurrent with loading; don't consider refresh 40 return value; 41 } 42 43 // immediately reuse invalid entries 44 writeQueue.remove(e); 45 accessQueue.remove(e); 46 this.count = newCount; // write-volatile 47 } 48 break; 49 } 50 } 51 //需要新建实体对象 52 if (createNewEntry) { 53 loadingValueReference = new LoadingValueReference (); 54 55 if (e == null) { 56 e = newEntry(key, hash, first); 57 e.setValueReference(loadingValueReference); 58 table.set(index, e);//把新的ReferenceEntry 引用实体对象添加进table 59 } else { 60 e.setValueReference(loadingValueReference); 61 } 62 } 63 } finally { 64 unlock(); 65 postWriteCleanup(); 66 } 67 //需要新建实体对象 68 if (createNewEntry) { 69 try { 70 // Synchronizes on the entry to allow failing fast when a recursive load is 71 // detected. This may be circumvented when an entry is copied, but will fail fast most 72 // of the time. 73 synchronized (e) {//同步重载数据 74 return loadSync(key, hash, loadingValueReference, loader); 75 } 76 } finally { 77 statsCounter.recordMisses(1); 78 } 79 } else { 80 // 重载中,说明实体已存在,等待重载完毕 81 return waitForLoadingValue(e, key, valueReference); 82 } 83 }
4.缓存回收机制
1)基于容量回收:CacheBuilder.maximumSize(long)
2)定时回收:
expireAfterAccess(long, TimeUnit):缓存项在给定时间内没有被读/写访问,则回收。
expireAfterWrite(long, TimeUnit):缓存项在给定时间内没有被写(创建或覆盖),则回收。
3)基于引用回收:
CacheBuilder.weakKeys():使用弱引用存储键。当键没有其它(强或软)引用时,缓存项可以被垃圾回收。因为垃圾回收仅依赖恒等式(==),使用弱引用键的缓存用==而不是equals比较键。
CacheBuilder.weakValues():使用弱引用存储值。当值没有其它(强或软)引用时,缓存项可以被垃圾回收。因为垃圾回收仅依赖恒等式(==),使用弱引用值的缓存用==而不是equals比较值。
CacheBuilder.softValues():使用软引用存储值。软引用只有在响应内存需要时,才按照全局最近最少使用的顺序回收。考虑到使用软引用的性能影响,我们通常建议使用更有性能预测性的缓存大小限定(见上文,基于容量回收)。使用软引用值的缓存同样用==而不是equals比较值。
4)显式清除:任何时候,你都可以显式地清除缓存项,而不是等到它被回收,具体如下
- 个别清除:Cache.invalidate(key)
- 批量清除:Cache.invalidateAll(keys)
- 清除所有缓存项:Cache.invalidateAll()
四、总结
优点:
- 采用锁分段技术,锁粒度减小,加大并发。
- API优雅,简单可用,支持多种回收方式。
- 自带统计功能。
缺点:
- 受内存大小限制不能存储太多数据
- 单JVM有效,非分布式缓存。多台服务可能有不同效果
参考:Guava Cache源码详解