平时工作中如果需要用到本地缓存的时候,相信大家的首选应该大多数是LoadingCache,毕竟Google出品,必属精品。常规的一些缓存场景LoadingCache都能应付,用过LoadingCache的朋友应该也都知道LoadingCache提供两种刷新机制,分别是expireAfterWrites和refreshAfterWrites,本文就用两个简洁明了的例子来说明下这两种刷新机制的用法。
第一段代码是测试expireAfterWrites,实现了CacheLoader的load方法,load方法中等待了10s后返回缓存值"China",另外启动两个线程每隔1s获取一次缓存
人狠话不多,直接上代码。
public class LoadingCacheExpiryTest {
public LoadingCache<String, String> loadingCache;
private ExecutorService executorService = Executors.newFixedThreadPool(5);
public LoadingCacheExpiryTest() {
loadingCache = CacheBuilder.newBuilder().expireAfterWrite(5, TimeUnit.SECONDS).build(
new CacheLoader<String, String>() {
private AtomicInteger count = new AtomicInteger(1);
private AtomicInteger reloadCount = new AtomicInteger(1);
@Override
public String load(String key) throws Exception {
System.out.println("Load Value " + count.get() + " time(s)");
for (int i = 0; i < 10; i++) {
System.out.println("Load Value for " + i + " second(s)");
Thread.currentThread().sleep(1000);
}
count.incrementAndGet();
return "China";
}
@Override
public ListenableFuture<String> reload(String key, String oldValue) throws Exception {
System.out.println("Reload for " + reloadCount.get() + " time(s)");
ListenableFutureTask<String> futureTask = ListenableFutureTask.create(() -> {
for (int i = 0; i < 10; i++) {
System.out.println("Reload Value for " + i + " second(s)");
Thread.currentThread().sleep(1000);
}
int count = reloadCount.incrementAndGet();
return "China" + count;
});
executorService.execute(futureTask);
return futureTask;
}
});
}
public static void main(String[] args) {
LoadingCacheExpiryTest test = new LoadingCacheExpiryTest();
Runnable runnable1 = () -> {
for (int i = 0; i < 100; i++) {
try {
System.out.println("Runnable1 Before Get Cache");
System.out.println("Runnable1 " + test.loadingCache.get("Country"));
System.out.println("Runnable1 After Get Cache");
Thread.currentThread().sleep(1000);
} catch (ExecutionException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Runnable runnable2 = () -> {
for (int i = 0; i < 100; i++) {
try {
System.out.println("Runnable2 Before Get Cache");
System.out.println("Runnable2 " + test.loadingCache.get("Country"));
System.out.println("Runnable2 After Get Cache");
Thread.currentThread().sleep(1000);
} catch (ExecutionException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread thread1 = new Thread(runnable1);
Thread thread2 = new Thread(runnable2);
thread1.start();
thread2.start();
}
}
测试结果:
Runnable2 Before Get Cache
Runnable1 Before Get Cache
Load Value 1 time(s) --进入load方法第一次获取缓存值,这里可以看出加载缓存是lazy load
Load Value for 0 second(s)
Load Value for 1 second(s)
Load Value for 2 second(s)
Load Value for 3 second(s)
Load Value for 4 second(s)
Load Value for 5 second(s)
Load Value for 6 second(s)
Load Value for 7 second(s)
Load Value for 8 second(s)
Load Value for 9 second(s) --缓存值获取完毕
Runnable2 China
Runnable2 After Get Cache --线程2获取到缓存值
Runnable1 China
Runnable1 After Get Cache
Runnable1 Before Get Cache
Runnable2 Before Get Cache
Runnable1 China --线程1获取到缓存值
Runnable1 After Get Cache
Runnable2 China
Runnable2 After Get Cache
Runnable1 Before Get Cache
Runnable2 Before Get Cache
Runnable2 China
Runnable2 After Get Cache
Runnable1 China
Runnable1 After Get Cache
Runnable2 Before Get Cache
Runnable1 Before Get Cache
Runnable2 China
Runnable2 After Get Cache
Runnable1 China
Runnable1 After Get Cache
Runnable1 Before Get Cache
Runnable2 Before Get Cache
Runnable1 China
Runnable1 After Get Cache
Runnable2 China
Runnable2 After Get Cache
Runnable2 Before Get Cache
Runnable1 Before Get Cache
Load Value 2 time(s) --到了缓存5s过期时间,重新调用load加载
Load Value for 0 second(s)
Load Value for 1 second(s)
Load Value for 2 second(s)
Load Value for 3 second(s)
Load Value for 4 second(s)
Load Value for 5 second(s)
Load Value for 6 second(s)
Load Value for 7 second(s)
Load Value for 8 second(s)
Load Value for 9 second(s)--第二次缓存值获取完成,可以看到这段过程中线程1和线程2都没有输出值,说明都block住了
Runnable1 China --线程1获取到缓存值
Runnable1 After Get Cache
Runnable2 China --线程1获取到缓存值
Runnable2 After Get Cache
Runnable2 Before Get Cache
结论1
使用了expireAfterWrites之后,每次缓存失效LoadingCache都会去调用我们实现的load方法去重新加载缓存,在加载期间,所有线程对该缓存key的访问都将被block。所以如果实际加载缓存需要较长时间的话,这种方式不太适用。从代码中还可以看到,即使在CacheLoader实现了reload方法,也不会被调用,因为reload只有当设置了refreshAfterWrites时才会被调用。
接下去我们再测试下refreshAfterWrites
话多人不狠,继续上代码
其实和第一段代码基本一致,只是使用了refreshAfterWrites
public class LoadingCacheRefreshTest {
public LoadingCache<String, String> loadingCache;
private ExecutorService executorService = Executors.newFixedThreadPool(5);
public LoadingCacheRefreshTest() {
loadingCache = CacheBuilder.newBuilder().refreshAfterWrite(5, TimeUnit.SECONDS).build(
new CacheLoader<String, String>() {
private AtomicInteger count = new AtomicInteger(1);
private AtomicInteger reloadCount = new AtomicInteger(1);
@Override
public String load(String key) throws Exception {
System.out.println("Load Value " + count.get() + " time(s)");
for (int i = 0; i < 10; i++) {
System.out.println("Load Value for " + i + " second(s)");
Thread.currentThread().sleep(1000);
}
count.incrementAndGet();
return "China";
}
@Override
public ListenableFuture<String> reload(String key, String oldValue) throws Exception {
System.out.println("Reload for " + reloadCount.get() + " time(s)");
ListenableFutureTask<String> futureTask = ListenableFutureTask.create(() -> {
for (int i = 0; i < 10; i++) {
System.out.println("Reload Value for " + i + " second(s)");
Thread.currentThread().sleep(1000);
}
int count = reloadCount.incrementAndGet();
return "China" + count;
});
executorService.execute(futureTask);
return futureTask;
}
});
}
public static void main(String[] args) {
LoadingCacheRefreshTest test = new LoadingCacheRefreshTest();
Runnable runnable1 = () -> {
for (int i = 0; i < 100; i++) {
try {
System.out.println("Runnable1 Before Get Cache");
System.out.println("Runnable1 " + test.loadingCache.get("Country"));
System.out.println("Runnable1 After Get Cache");
Thread.currentThread().sleep(1000);
} catch (ExecutionException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Runnable runnable2 = () -> {
for (int i = 0; i < 100; i++) {
try {
System.out.println("Runnable2 Before Get Cache");
System.out.println("Runnable2 " + test.loadingCache.get("Country"));
System.out.println("Runnable2 After Get Cache");
Thread.currentThread().sleep(1000);
} catch (ExecutionException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread thread1 = new Thread(runnable1);
Thread thread2 = new Thread(runnable2);
thread1.start();
thread2.start();
}
}
测试结果
Runnable1 Before Get Cache
Runnable2 Before Get Cache
Load Value 1 time(s) --第一次加载缓存,调用load方法
Load Value for 0 second(s)
Load Value for 1 second(s)
Load Value for 2 second(s)
Load Value for 3 second(s)
Load Value for 4 second(s)
Load Value for 5 second(s)
Load Value for 6 second(s)
Load Value for 7 second(s)
Load Value for 8 second(s)
Load Value for 9 second(s)
Runnable2 China --线程2获取到缓存值China
Runnable2 After Get Cache
Runnable1 China --线程1获取到缓存值China
Runnable1 After Get Cache
Runnable1 Before Get Cache
Runnable2 Before Get Cache
Runnable1 China
Runnable1 After Get Cache
Runnable2 China
Runnable2 After Get Cache
Runnable2 Before Get Cache
Runnable1 Before Get Cache
Runnable2 China
Runnable2 After Get Cache
Runnable1 China
Runnable1 After Get Cache
Runnable1 Before Get Cache
Runnable2 Before Get Cache
Runnable1 China
Runnable1 After Get Cache
Runnable2 China
Runnable2 After Get Cache
Runnable2 Before Get Cache
Runnable1 Before Get Cache
Runnable2 China
Runnable2 After Get Cache
Runnable1 China
Runnable1 After Get Cache
Runnable2 Before Get Cache
Runnable1 Before Get Cache
Reload for 1 time(s) --缓存过期,调用realod方法刷新缓存
Runnable1 China --注意看,这里的线程1仍然可以获取到China,而不会被block
Runnable1 After Get Cache
Reload Value for 0 second(s)
Runnable2 China --注意看,这里的线程2仍然可以获取到China,而不会被block
Runnable2 After Get Cache
Runnable1 Before Get Cache
Runnable2 Before Get Cache
Runnable2 China
Runnable2 After Get Cache
Reload Value for 1 second(s)--缓存加载ing
Runnable1 China
Runnable1 After Get Cache
Runnable2 Before Get Cache
Runnable1 Before Get Cache
Runnable1 China --注意看,这里的线程1仍然可以获取到China,而不会被block
Runnable1 After Get Cache
Reload Value for 2 second(s)--缓存加载ing
Runnable2 China --注意看,这里的线程2仍然可以获取到China,而不会被block
Runnable2 After Get Cache
Runnable1 Before Get Cache
Runnable2 Before Get Cache
Reload Value for 3 second(s)
Runnable2 China
Runnable2 After Get Cache
Runnable1 China
Runnable1 After Get Cache
Runnable1 Before Get Cache
Runnable1 China
Runnable1 After Get Cache
Runnable2 Before Get Cache
Runnable2 China
Runnable2 After Get Cache
Reload Value for 4 second(s)
Runnable1 Before Get Cache
Runnable2 Before Get Cache
Runnable1 China
Runnable1 After Get Cache
Reload Value for 5 second(s)
Runnable2 China
Runnable2 After Get Cache
Runnable1 Before Get Cache
Reload Value for 6 second(s)
Runnable2 Before Get Cache
Runnable1 China
Runnable1 After Get Cache
Runnable2 China
Runnable2 After Get Cache
Runnable2 Before Get Cache
Reload Value for 7 second(s)
Runnable1 Before Get Cache
Runnable2 China
Runnable2 After Get Cache
Runnable1 China
Runnable1 After Get Cache
Runnable2 Before Get Cache
Runnable1 Before Get Cache
Reload Value for 8 second(s)
Runnable1 China
Runnable1 After Get Cache
Runnable2 China
Runnable2 After Get Cache
Runnable2 Before Get Cache
Runnable1 Before Get Cache
Reload Value for 9 second(s)
Runnable1 China
Runnable1 After Get Cache
Runnable2 China
Runnable2 After Get Cache
Runnable2 Before Get Cache
Runnable1 Before Get Cache
Runnable2 China
Runnable2 After Get Cache
Runnable1 China
Runnable1 After Get Cache
Runnable2 Before Get Cache
Runnable1 Before Get Cache
Runnable2 China2 --缓存加载完毕,线程2获取到了最新的缓存值China2
Runnable2 After Get Cache
Runnable1 China2 --缓存加载完毕,线程1获取到了最新的缓存值China2
Runnable1 After Get Cache
Runnable2 Before Get Cache
Runnable1 Before Get Cache
Runnable2 China2
Runnable2 After Get Cache
Runnable1 China2
结论2
注意看代码,适用了refreshAfterWrites之后,需要自己实现CacheLoader的reload方法,在方法中创建一个ListenableFutureTask,然后将这个task提交给线程池去异步执行,reload方法最后返回这个ListenableFutureTask。这样做之后,缓存失效后重新加载就变成了异步,加载期间尝试获取缓存的线程也都不会被block,而是获取到加载之前的值。当加载完毕之后,各个线程就能取到最新值了。
从这两个例子可以看出,对于互联网高并发的场景,refreshAfterWrites这种使用异步刷新缓存的方法应该是我们首先考虑的,取到一个过期的旧值总比大量线程全都被block要好,后者可能会导致请求大量堆积,连接数不够等一些列问题。