一、背景

在项目中,为提升系统性能,减少不必要读数据库或读redis开销,本地缓存是必不可少的。同时又存在很多缓存工具,其中最常用的是Guava和Caffeine,Caffeine是基于Google Guava Cache设计经验上改进的成果,Caffeine比guava性能和命中率更具优势《Caffeine对比Guava优势》。根据调研,OKCoin很多项目使用Guava《okcoin使用本地缓存情况》,为提升性能、减少出现使用上的问题和适应未来维护,很有必要将Caffeine做为本地缓存,并规范Caffeine使用;

二、目标

1.新项目需使用本地缓存场景,统一使用Caffeine;
2.使用Guava项目,切换到Caffeine;
3.规范使用Caffeine,减少使用过程中出现的问题;

三、使用规范

3.1 主要api说明

序号 api 详细说明 使用规范 备注
1 initialCapacity() 初始容量,设置内部ConcurrentHashMap的初始值 设置初始容量值为最大容量,防止ConcurrentHashMap扩容影响性能
2 maximumSize() 最大容量 根据热数据量,同时考虑内存占用
3 expireAfterWrite() 代表着写了之后多久过期,写过期后,会对key加锁同步执行load,未执行完load,非获取锁线程阻塞等待,有可能会造成程序阻塞 过期会“同步”加载数据,造成阻塞
4 expireAfterAccess() 代表着最后一次访问了之后多久过期,访问过期后,会对key加锁同步执行load,未执行完load,非获取锁线程阻塞等待,有可能会造成程序阻塞
5 expireAfter() 在expireAfter中需要自己实现Expiry接口,这个接口支持create,update,以及access了之后多久过期。注意这个API和前面两个API是互斥的。这里和前面两个API不同的是,需要你告诉缓存框架,他应该在具体的某个时间过期,也就是通过前面的重写create,update,以及access的方法,获取具体的过期时间。
6 refreshAfterWrite() 写多久之后刷新,不会主动执行,刷新操作的触发时机是在数据读取之后,通过判断当前时间减去数据的创建时间是否大于refreshAfterWrite指定的时间,如果大于则进行异步刷新操作,不会造成程序阻塞;默认的刷新线程池是ForkJoinPool.commonPool(),也可以通过executor方法指定为其它线程池。 过期会“异步”加载数据,未加载完成,“返回旧数据”
7 recordStats() 统计记录
8 removalListener() 移除监听

3.2 get缓存运作流程

设置了expireAfterWrite 和 refreshAfterWrite,expireAfterWrite>refreshAfterWrite;
Caffeine Cache - 图1

3.3 缓存场景和设置

名词说明

  • 数据写缓存时间:get 请求缓存时,当前时间 - 该缓存上次写入时间;
  • 写刷新时间:Caffeine 初始化时,设置的 refreshAfterWrite;
  • 写过期时间:Caffeine 初始化时,设置的 expireAfterWrite;
  • 缓存中旧值:在get操作时,获取到的数据不在刷新时间范围内的数据;

使用场景 1 使用 map;

存在问题:
需手动实现容量限制、过期删除;

适用业务场景:

  1. 缓存数据量少,不用限制缓存占用内存容量;
  2. 缓存值不变,不需要刷新缓存;

使用场景2 使用 map,定时任务异步刷新数据;

存在问题:
需手动实现容量限制、过期删除;手动定时任务异步刷新缓存;

适用业务场景:

  1. 缓存数据量少,不用限制缓存占用内存容量;
  2. 缓存值会变,需刷新缓存;

使用业务:
永续,法币,矿池

使用场景3 使用 Caffeine,设置最大容量,只设置 refreshAfterWrite;不设置 expireAfterWrite;

get缓存原理图:
Caffeine Cache - 图2
存在问题:
get缓存间隔超refreshAfterWrite后,触发缓存异步刷新,此时会获取缓存中的旧值;

适用业务场景:
1.缓存数据量大,限制缓存占用内存容量;
2.缓存值会变,需刷新缓存;
3.可以接受任何时间缓存中旧数据;
推荐设置:
refreshAfterWrite=1s;不同业务根据需求设置为可接受的缓存源中数据更新间隔;

使用场景4 使用Caffeine,设置最大容量,设置refreshAfterWrite为刷新时间;设置expireAfterWrite为可接受超时时间范围,refreshAfterWrite < expireAfterWrite;

get缓存原理图:
Caffeine Cache - 图3
存在问题:
get缓存间隔在refreshAfterWrite和expireAfterWrite之间,触发缓存异步刷新,此时会获取缓存中旧值;
get缓存间隔大于expireAfterWrite,针对于该key,获取到锁的线程会同步执行load,其他未获取锁线程会阻塞等待,获取锁线程执行延时过长会造成其他线程阻塞时间过长;
使用业务场景:
1.缓存数据量大,限制缓存占用内存容量;
2.缓存值会变,需刷新缓存;
3.可以接受缓存中有限时间范围旧数据;
4.同步加载数据延迟小(使用redis或数据库能快速读取数据);
使用业务:
保险柜,浏览器,矿池,法币
推荐设置:
refreshAfterWrite=1s, expireAfterWrite=3s;不同业务根据需求设置refreshAfterWrite为可接受的缓存源中数据更新间隔,设置expireAfterWrite为可接受超时时间范围;

使用场景5 使用Caffeine,设置最大容量,不设置refreshAfterWrite,不设置expireAfterWrite,定时任务异步刷新数据;

get缓存原理图:
Caffeine Cache - 图4
存在问题:
需手动定时任务异步刷新缓存;
适用业务场景:
1.缓存数据量大,限制缓存占用内存容量;
2.缓存值会变,需刷新缓存;
3.不可以接受缓存中旧数据(get获取缓存时,获取到的是手动定时异步刷新缓存间隔内的数据);
4.同步加载数据延迟可能会很大;
使用业务:
浏览器
推荐设置:
定时任务异步刷新数据间隔为1s;不同业务根据需求定时任务间隔为可接受的缓存源中数据更新间隔;

使用场景6 使用Caffeine,设置最大容量,不设置refreshAfterWrite,设置expireAfterWrite;

get缓存原理图:
Caffeine Cache - 图5
存在问题:
get缓存间隔大于expireAfterWrite,针对于该key,获取到锁的线程会同步执行load,其他未获取锁线程会阻塞等待,获取锁线程执行延时过长会造成其他线程阻塞时间过长;
适用业务场景:
1.缓存数据量大,限制缓存占用内存容量;
2.缓存值会变,需刷新缓存;
3.不可以接受缓存中旧数据,
4.同步加载数据延迟小(使用redis或数据库能快速读取数据);
推荐设置
expireAfterWrite=3;不同业务根据需求,设置expireAfterWrite为可接受超时时间范围;

四、监控指标

1.最大容量 → 内存占用
2.缓存数量
3.命中率
4.请求次数
5.命中次数
6.移除监听
7.更新频率

五、项目集成

5.1 新项目使用caffeine

1.引入pom依赖
com.github.ben-manes.caffeine caffeine
2.初始化caffeine缓存
private static final LoadingCache build = Caffeine.newBuilder() .initialCapacity(1024).maximumSize(1024) .refreshAfterWrite(200, TimeUnit.MILLISECONDS) .expireAfterWrite(10, TimeUnit.SECONDS) .removalListener((k,v,c) -> { }) .build(k -> { return k; });

5.2 Guava切换到Caffeine

1.pom依赖切换
1.1 添加caffeine依赖
com.github.ben-manes.caffeine caffeine
1.2 如果项目没有使用guava的其他类则可以删除guava依赖
com.google.guava guava
2.使用方切换
caffeine兼容guava的api,切换方式非常简单,把CacheBuilder.newBuilder()改为Caffeine.newBuilder(),api不用变;

六、问题总结

1.refreshAfterWrite设置的时间大于expireAfterWrite,导致refreshAfterWrite不生效;
一般refreshAfterWrite常和expireAfterWrite结合使用,需要注意的是:refreshAfterWrite设置的时间要小于expireAfterWrite,因为在读取数据的时候首先通过expireAfterWrite来判断数据有没有失效,数据失效后会同步更新数据,如果refreshAfterWrite时间大于expireAfterWrite,那么refresh操作永远不会执行到,设置了refreshAfterWrite也没有任何意义。
2.refreshAfterWrite需要缓存操作才触发缓存刷新,由于是异步刷新,未执行完刷新会返回缓存中旧数据,注意可以接受缓存中旧数据的时间范围;
3.expireAfterWrite会同步执行load,突发流量下,多并发执行load会对系统稳定性造成影响;