# Cache常见应用问题与Guava LoadingCache解决方案
标签(空格分隔): Java-其他库 架构
---
[TG架构笔记][1]
---
## 经典缓存写法
```java
private Map cache = new ConcurenthashMap();
Object getFromCache(String Key){
Object value = cache.get(key);
if(value == null){
value = getFromSource(key);
cache.put(key, value);
}
}
```
### 并发问题
多线程并发访问`if(value == null)`,只有第一条线程加载数据,后续线程才能命中缓存;反之,如果`getFromSource()`方法请求资源有阻塞,所有流量都会通过`value == null`的判断冲击请求资源,缓存根本没有起到保护资源的作用;解决方案:加载数据时,需要加锁,避免多线程同时装载;
针对以上问题,可以通过加锁避免并发装载数据,对于同一份需要装载的数据,需要加锁避免多个线程同时装载。一个线程在等待装载数据,其他线程应该等待它完成装载。数据装载完成之后,等待中的其他工作线程应该直接使用新装载的数据
```java
private Map cache = new ConcurenthashMap();
Object getFromCache(String Key){
lock(this) { // or sync
Object value = cache.get(key);
if(value == null){
value = getFromSource(key);
cache.put(key, value);
}
}
}
```
LoadingCache解决方案:
```java
@Test
public void testMultiThread() throws InterruptedException {
LoadingCache cache = CacheBuilder.newBuilder()
//缓存项在创建后,在给定时间内没有被读/写访问,则清除。
.expireAfterAccess(100, TimeUnit.MILLISECONDS)
.build(new CacheLoader() {
@Override
public String load(String key) throws Exception {
System.out.println("loading from CacheLoader datasource ");
Thread.sleep(500);
return "target value";
}
});
//并发装载资源,线程自动挂起等待
Thread thread1 = new Thread(() -> getAndReload(cache));
Thread thread2 = new Thread(() -> getAndReload(cache));
thread1.start();
thread2.start();
Thread.sleep(2000);
//output :
//loading from CacheLoader datasource
//Thread-1:target value
//Thread-2:target value
}
```
断崖式下滑问题:
缓存的数据更新逻辑和数据在缓存中的存在被绑死
只有当缓存数据被清理时,才有机会更新数据
而数据被清理时,请求拿不到旧数据被迫等待,造成停顿
解决方案:始终只有一条线程更新缓存数据,而其他线程不阻塞不等待,直接获取缓存数据。缓存过期策略调整为不过期,而是进程主动更新缓存。
loadingCache解决方案:
```java
@Test
public void testMultiThreadAndReloadAsync() throws InterruptedException {
LoadingCache cache = CacheBuilder.newBuilder()
.refreshAfterWrite(100, TimeUnit.MILLISECONDS)
.removalListener(new RemovalListener() {
@Override
public void onRemoval(RemovalNotification removalNotification) {
System.out.println(Thread.currentThread().getName() + "-remove key:" + removalNotification.getKey());
System.out.println(Thread.currentThread().getName() + "-remove value:" + removalNotification.getValue());
}
})
.build(new CacheLoader() {
@Override
public String load(String key) throws InterruptedException {
System.out.println(Thread.currentThread().getName() + "start loading");
value++;
String output = String.valueOf(value);
Thread.sleep(1000L);
System.out.println(Thread.currentThread().getName() + "load from db:" + output);
return output;
}
});
//此外需要注意一个点,这里的定时并不是真正意义上的定时。Guava cache的刷新需要依靠用户请求线程,让该线程去进行load方法的调用,所以如果一直没有用户尝试获取该缓存值,则该缓存也并不会刷新。
for (int i = 0; i < 5; i++) {
Thread thread3 = new Thread(() -> {
while (true) {
try {
Thread.sleep((long) (Math.random() * 1000));
getAndReload(cache);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
});
thread3.start();
}
Thread.sleep(800000L);
}
private void getAndReload(LoadingCache cache) {
try {
String result = cache.get("key");
System.out.println(Thread.currentThread().getName() + ":get from cache:" + result);
} catch (ExecutionException ex) {
ex.printStackTrace();
}
}
```
### 过期策略
1. 缓存未设置过期时间,两种缓存时间过期机制:1)、不再读写的指定时间后删除;2)、指定写入一定时间后删除;3)、指定绝对时间后过期
2. 业务保护、动态过期策略
### 业务保护
未完待续
## loadingCache
### get()与getIfPresent()区别
1. `V get(K k)`: 内部调用getOrLoad(K key)方法,缓存中有对应的值则返回,没有则使用CacheLoader load方法getOrLoad(K key)方法为线程安全方法,内部加锁
2. `V getIfPresent(Object key)`:缓存中有对应的值则返回,没有则返回NULL
### expireAfterAccess(expireAfterWrite)与refreshAfterWrite
1. expireAfterAccess:缓存项在创建后,在给定时间内没有被读/写访问,则清除
2. expireAfterWrite:缓存项在创建后,在给定时间内没有写访问,则清除
3. refreshAfterWrite:缓存项在创建后,定时从cacheLoader检索data并刷新缓存
> 注意:理论上,expire的机制是只要给定时间内一直有读或写的访问,本地缓存就不会过期
通过定时刷新可以让缓存项保持可用,但请注意:缓存项只有在被检索时才会真正刷新,
即只有刷新间隔时间到了你再去get(key)才会重新去执行Loading否则就算刷新间隔时间到了也不会执行loading操作。因此,如你在缓存上同时声明expireAfterWrite和refreshAfterWrite,缓存并不会因为刷新盲目地定时重置如果缓存项没有被检索,那刷新就不会真的发生,缓存项在过期时间后也变得可以回收。
```java
@Test
public void testLoadingCacheExpireAfterAccess2() throws InterruptedException, ExecutionException {
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setNameFormat("thread-%d")
.build();
ExecutorService executorService = Executors.newCachedThreadPool(threadFactory);
int expireAfterAccess = 300;
int refreshAfterWrite = 200;
LoadingCache cache = CacheBuilder.newBuilder()
//缓存项在创建后,在给定时间内没有被读/写访问,则清除。
.expireAfterAccess(expireAfterAccess, TimeUnit.MILLISECONDS)
.refreshAfterWrite(refreshAfterWrite, TimeUnit.MILLISECONDS)
.recordStats()
.build(new CacheLoader() {
@Override
public String load(String key) throws Exception {
System.out.println("loading from CacheLoader datasource ");
return "target value";
}
});
Future submit = executorService
.submit(() -> {
while (true) {
String value = cache.get("key");
System.out.println(value);
System.out.println(cache.stats());
try {
//线程挂起时长超过300毫秒,缓存会过期,从cacheLoader中加载
//反之,若少于300毫秒,缓存永远不会过期
TimeUnit.MILLISECONDS.sleep(50);
} catch (InterruptedException e1) {
Thread.currentThread().interrupt();
}
}
});
Thread.sleep(2000);
submit.cancel(true);
Thread.sleep(500);
String value = cache.get("key");
System.out.println("last:" + value);
System.out.println("last:" + cache.stats());
}
```
###
[1]: https://www.zybuluo.com/zero1036/note/599284