生产问题之CaffeineCache使用中的坑
问题描述
服务缓存中使用CaffeineCache时,通过CaffeineCache在数据更新并且超过refreshAfterWrite时间后首次获取缓存时,偶发性的获取不到最新的缓存
实现
public class CaffeineCacheTest {
public static void main(String[] args) throws Exception {
CaffeineCacheTest test = new CaffeineCacheTest();
Cache cache = test.caffeineCacheManager().getCache("test");
CacheObject cacheObject = (CacheObject) cache.get("key1").get();
System.out.println(cacheObject.val);
Thread.sleep(6000);
System.out.println("sleep1 over!");
cacheObject = (CacheObject)cache.get("key1").get();
System.out.println(cacheObject.val);
Thread.sleep(3000);
System.out.println("sleep2 over!");
cacheObject = (CacheObject)cache.get("key1").get();
System.out.println(cacheObject.val);
cacheObject = (CacheObject)cache.get("key1").get();
System.out.println(cacheObject.val);
cacheObject = (CacheObject)cache.get("key2").get();
System.out.println(cacheObject.val);
}
public CacheManager caffeineCacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
//创建或更新之后多久刷新,需要设置cacheLoader
.refreshAfterWrite(5, TimeUnit.SECONDS)
.maximumSize(1000));
cacheManager.setCacheLoader(new CacheLoader<Object, Object>() {
@Override
public Object load(Object key) throws Exception {
System.out.println("load");
CacheObject cacheObject = new CacheObject(key + "," + System.currentTimeMillis());
return cacheObject;
}
// 重写这个方法将oldValue值返回回去,进而刷新缓存
@Override
public Object reload(Object key, Object oldValue) throws Exception {
System.out.println("reload");
//Thread.sleep(1000);
CacheObject cacheObject = (CacheObject)oldValue;
cacheObject.val = key + "," + System.currentTimeMillis();
return cacheObject;
}
});
return cacheManager;
}
}
class CacheObject {
String val;
public CacheObject(String val) {
this.val = val;
}
}
- 设置缓存刷新时间为5s
- 设置最大缓存数据量为1000
- 在load和reload时返回保存当前时间戳的对象
原因
在首次获取缓存6s后再次获取,如果在reload中sleep 1s,则获取到旧的缓存数据,如果不sleep 1s则获取到新的数据
故CaffeineCacheManager是异常refresh缓存,如果在获取缓存数据时,已经refresh完成,则响应最新的,否则获取老的缓存数据
调整方案
不处理
不处理时,只有调整后首次访问到老的数据,再访问时获取最新数据,在请求量比较大时,延迟很小,可以忽略
调整refresh为expire
cacheManager.setCaffeine(Caffeine.newBuilder()
//创建或更新之后多久刷新,需要设置cacheLoader
//.refreshAfterWrite(5, TimeUnit.SECONDS)
.expireAfterWrite(5, TimeUnit.SECONDS)
/*.executor(new Executor() {
@Override
public void execute(Runnable command) {
command.run();
}
})*/
.maximumSize(1000));
将refreshAfterWrite调整为expireAfterWrite,这样每次过期后,都会执行load而非reload,load方法是同步的,故不会出现同时get和load的情况
调整Executor
cacheManager.setCaffeine(Caffeine.newBuilder()
//创建或更新之后多久刷新,需要设置cacheLoader
.refreshAfterWrite(5, TimeUnit.SECONDS)
//.expireAfterWrite(5, TimeUnit.SECONDS)
.executor(new Executor() {
@Override
public void execute(Runnable command) {
command.run();
}
})
.maximumSize(1000));
调整executor为当前线程,则refresh和获取缓存在同一线程,故先refresh后再获取缓存,不会同时处理get和reload的情况
优劣分析
- 三种方案各有优劣,具体使用哪种方案,根据业务判断
- 不处理的优点是load失败,并不影响当前获取缓存,只是获取的是老的缓存,缺点就是缓存实时性问题
- 调整refresh为expire 优点是过期即load,保证缓存实时性问题,但是load即会有延时或获取时io异常问题
- 调整Executor优缺点同调整refresh为expire
- 出问题的业务接口存在两种场景
- 场景1,流量高,对于该场景而言,方案一虽然获取老的缓存,但是能够及时在下一次获取新的缓存,影响不大
- 场景2,流量低,对于该场景,如果使用方案一可能会导致业务异常