Caffeine本地缓存-策略测试

625 阅读4分钟

Caffeine是一个高性能的本地缓存库,提供了四种类型的Cache,对应着四种加载策略:手动加载(Cache)、自动加载(LoadingCache)、手动异步加载(AsyncCache)和自动异步加载(AsyncLoadingCache); 相较于Redis,Caffeine可以从策略配置上来避免缓存击穿(key过期后大流量并发访问场景)。

1. 策略配置效果

策略配置效果:【已验证】 image.png

2. 测试Demo

注意load实现中getByActivityId0()方法,num值自增!

@Slf4j
@RestController
@RequestMapping("/caffeine")
public class CaffeineController {

    @Autowired
    private ActivityClientRepositoryImpl activityClientRepository;

    @PostMapping("/get")
    public Object query(String id) {
        LoadingCache<String, ActivityModel> activityCache = activityClientRepository.getActivityCache();
        System.out.println("before cache=" + activityCache.asMap());
        ActivityModel model = activityClientRepository.getActivityFromCache(id);
        System.out.println("after cache=" + activityCache.asMap());
        return model;
    }
}
@Data
@Repository
public class ActivityClientRepositoryImpl {

    private long activityCacheMaximumSize;
    private int activityCacheExpireAfterAccess;
    private int activityCacheRefreshAfterWrite;

    public LoadingCache<String, ActivityModel> activityCache;

    private AtomicInteger count = new AtomicInteger(0);

    @PostConstruct
    private void init() {
        activityCache = Caffeine.newBuilder()
                .maximumSize(activityCacheMaximumSize)
                .expireAfterAccess(20, TimeUnit.SECONDS)
                .refreshAfterWrite(5, TimeUnit.SECONDS)
                .build(this::getByActivityId0);  // 自动加载策略
    }

    private ActivityModel getByActivityId0(String activityId) throws InterruptedException {
        //TimeUnit.SECONDS.sleep(1);
        System.out.println("load key=" + activityId);
        ActivityModel activity = new ActivityModel();
        activity.setNum(count.incrementAndGet());
        return activity;
    }

    public ActivityModel getActivityFromCache(String activityId) {
        if (activityId == null) {
            return null;
        }
        return activityCache.get(activityId);
    }

    @Value("${scd.activityCache.maximumSize:10000}")
    public void setActivityCacheMaximumSize(long maximumSize) {
        if (activityCache != null) {   // Caffeine支持在程序运行过程中策略的配置!
            activityCache.policy().eviction().ifPresent(eviction -> {
                eviction.setMaximum(maximumSize);
            });
        }
        this.activityCacheMaximumSize = maximumSize;
    }

    @Value("${scd.activityCache.expireAfterAccess:3000}")
    public void setActivityCacheExpireAfterAccess(int expireAfterAccess) {
        if (activityCache != null) {
            activityCache.policy().expireAfterAccess().ifPresent(expiration -> {
                expiration.setExpiresAfter(expireAfterAccess, TimeUnit.SECONDS);
            });
        }
        this.activityCacheExpireAfterAccess = expireAfterAccess;
    }

    @Value("${scd.activityCache.refreshAfterWrite:10}")
    public void setActivityCacheRefreshAfterWrite(int refreshAfterWrite) {
        if (activityCache != null) {
            activityCache.policy().refreshAfterWrite().ifPresent(refresh -> {
                refresh.setExpiresAfter(refreshAfterWrite, TimeUnit.SECONDS);
            });
        }
        this.activityCacheRefreshAfterWrite = refreshAfterWrite;
    }
}

3. 测试效果

3.1 LoadingCache自身特性

LoadingCache特性
1. key不存在时,调用get(key)方法会执行load方法进行加载;
    before cache={}
    load key=10 
    after cache={10=ActivityModel(num=1)}

2. key不存在时,两个线程同时执行get(key)方法,后一线程被阻塞,直至前一线程更新缓存完成!

3.2 expireAfterAccess驱逐策略

expireAfterAccess驱逐策略
策略配置:.expireAfterAccess(10, TimeUnit.SECONDS)

1. key过期后,调用get(key)方法也会执行load方法进行加载;
    // 0s: get(10)
    before cache={}
    load key=10
    after cache={10=ActivityModel(num=1)}

    // 10s后:get(10)
    before cache={}
    load key=10
    after cache={10=ActivityModel(num=2)}

3.3 refreshAfterWrite刷新策略

refreshAfterWrite刷新策略:
策略配置:.refreshAfterWrite(10, TimeUnit.SECONDS)
// 为了效果明显,load实现中加了TimeUnit.SECONDS.sleep(5);

1. key过期后,第一次get(key)拿到的是旧值,同时异步调用load方法进行刷新

    // 0s: get(11)
    before cache={}
    load key=11
    after cache={11=ActivityModel(num=1)}

    // 10s后:第一次get(11):
    before cache={11=ActivityModel(num=1)}
    after cache={11=ActivityModel(num=1)}  // 这里:先返回,后异步load
    load key=11

2. key过期后,第一次get(key)拿到的是旧值,紧接着get(key)拿到的也是旧值,直到上一次的load刷新成功
    // 0s: get(11)
    before cache={}
    load key=11
    after cache={11=ActivityModel(num=1)}

    // 10s后:第一次get(11):
    before cache={11=ActivityModel(num=1)}
    after cache={11=ActivityModel(num=1)}  // 这里:先返回,后异步load

    // 紧接着的一次get(11):
    before cache={11=ActivityModel(num=1)}
    after cache={11=ActivityModel(num=1)}
    
    load key=11 // 过了一会,第一次get时完成了异步load

3.4 混合双打

策略配置:
    .expireAfterAccess(20, TimeUnit.SECONDS)
    .refreshAfterWrite(5, TimeUnit.SECONDS)
// 为了效果明显,load实现中加了TimeUnit.SECONDS.sleep(1);

1. 直接模拟走一遍
    // 0s: get(21)
    before cache={}
    load key=21  // key不存在,同步load
    after cache={21=ActivityModel(num=1)}

    // 1s: get(21)
    before cache={21=ActivityModel(num=1)}
    after cache={21=ActivityModel(num=1)}

    // 6s: get(21)
    before cache={21=ActivityModel(num=1)}
    after cache={21=ActivityModel(num=1)} // 返回旧值,异步load
    load key=21

    // 8s: get(21)
    before cache={21=ActivityModel(num=2)}
    after cache={21=ActivityModel(num=2)}

    // 28s: get(21)
    before cache={}
    load key=21 // key过期了,同步load
    after cache={21=ActivityModel(num=3)}

4. 总结

image.png

Refer:Guava Cache特性:refreshAfterWrite与expireAfterWrite

5. cache.getAll(keys) 测试

@PostMapping("/getAll")
public Object queryAll(String ids) {
    System.out.println("before cache=" + activityCache.asMap());
    String[] split = ids.split(",");
    List<String> collect = Arrays.stream(split).collect(Collectors.toList());
    Map<String, ActivityModel> all = activityCache.getAll(collect);
    System.out.println("after cache=" + activityCache.asMap());
    return all;
}


@PostConstruct
private void init() {
    activityCache = Caffeine.newBuilder()
            .maximumSize(1024)
            .expireAfterAccess(200, TimeUnit.SECONDS)
            .refreshAfterWrite(50, TimeUnit.SECONDS)
            .build(new CacheLoader<String, ActivityModel>() {

                @Nullable
                @Override
                public ActivityModel load(@NotNull String key) throws Exception {
                    return getByActivityId0(key);
                }

                @NotNull
                @Override
                public Map<String, ActivityModel> loadAll(@NotNull Iterable<? extends String> keys) throws Exception {
                    System.out.println("loadAll keys=" + keys);
                    Map<String, ActivityModel> map = new HashMap<>();
                    for (String key : keys) {
                        ActivityModel activityModel = new ActivityModel();
                        activityModel.setNum(Integer.valueOf(key));
                        map.put(key, activityModel);
                    }
                    return map;
                }

            });
}

结论:
1. 未覆盖loadAll方法时,调用getAll(keys)方法时会对不存在的key值,依次调用load方法,同时返回值放入cache
    // get(1)
    before cache={}
    load key=1
    after cache={1=ActivityModel(num=1)}

    // getAll(1,2,3)
    before cache={1=ActivityModel(num=1)}
    load key=2
    load key=3
    after cache={1=ActivityModel(num=1), 2=ActivityModel(num=2), 3=ActivityModel(num=3)}
    
    
2. 覆盖loadAll方法时,调用getAll(keys)时,会对不存在的keys,调用loadAll方法,同时返回值放入cache
    // get(1)
    before cache={}
    load key=1
    after cache={1=ActivityModel(num=1)}
    // getAll(1,2,3)
    before cache={1=ActivityModel(num=1)}
    loadAll keys=[2, 3]
    after cache={1=ActivityModel(num=1), 2=ActivityModel(num=2), 3=ActivityModel(num=3)}

Refer:Caffeine批量加载浅析 (实际应用场景)