生产问题之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,流量低,对于该场景,如果使用方案一可能会导致业务异常