Background

对于服务消费,响应时间(RT)的长短是衡量一个系统高效处理业务的重要指标,缓存就是必不可少的优化工具,在一个高并发的场景中往往占有着非常重要的角色,所以开发人员需要根据不同的应用场景来选择不同的缓存框架,比如分布式缓存redis,或者内存缓存GuavaCache。

我们团队有一个调用比较频繁的服务,之前是和别的团队共用的Redis缓存服务,前不久突然得到消息,维护该redis团队要收回使用权,而我们服务使用也比较简单,只是用来做一个接口调用鉴权,从redis中拿出所有授权方的appKey,然后校验调用方的appKey是否合法。所以我第一想法是采用jvm本地缓存,因为这个调用方不会很多,而且数据是比较静态的,变化不会很频繁,放在jvm本地内存中,也不会占用很大空间,而且也不用担心多个部署实例缓存不一致的问题,经过调用有很多比较优秀的本地缓存比如GuavaCache,Caffeine等。我最后采用了Caffeine。

What is Caffeine?

内存缓存与Map之间的本质区别就是能自动的回收存储的元素,而GuavaCache是一款非常优秀的内存缓存框架,很好的提供了读写和自动失效的功能。而今天要介绍的内存缓存Caffeine,在设计上参考了GuavaCache的经验,也进行了大量的改进优化,除了之前提到的GuavaCache的优点,还可以支持自动刷新,失效后自动加载等优点。以下数据图片均来源于Caffeine GitHub,首先是读写性能的比较:
8个进程读取

8个线程同时从缓存中读取

8个进程写入
8个线程同时从缓存中写入

8个进程写入
6个线程读取,2个线程写入

从上面的测试结果图,我们可以看出caffeine在读写方面明显优与其他框架,在缓存命中率上Caffeine也不同于Guava,采用了更为优秀的Window TinyLfu算法,该算法是在LRU的基础上改进的版本。

Feature

填充策略

  • 1、手动填充

    Caffeine.newBuilder()方法只是Caffeine类的一个空的构造函数,类属性的实例化是在build方法中进行的,put方法就是手动填充缓存。newBuilder方法后面还能跟很多配置方法,比如

    Cache<String, Map<String, String>> cache = Caffeine.newBuilder().maximumSize(5000).expireAfterWrite(172800L, TimeUnit.MILLISECONDS).build();
    Map<String, String> value = Maps.newHashMap();
    cache.put("key", value)

我们也可以使用 get 方法获取值,该方法将一个参数为 key 的 Function 作为参数传入。如果缓存中不存在该 key,则该函数将用于提供默认值,该值在计算后插入缓存中。
Caffeine类是Caffeine的基础类,里面提供了很多配置方法和参数:

maximumSize:设置缓存最大条目数,超过条目则触发回收。 
maximumWeight:设置缓存最大权重,设置权重是通过weigher方法, 需要注意的是权重也是限制缓存大小的参数,并不会影响缓存淘汰策略,也不能和maximumSize方法一起使用。
weakKeys:将key设置为弱引用,在GC时可以直接淘汰
weakValues:将value设置为弱引用,在GC时可以直接淘汰
softValues:将value设置为软引用,在内存溢出前可以直接淘汰
expireAfterWrite:写入后隔段时间过期
expireAfterAccess:访问后隔断时间过期
refreshAfterWrite:写入后隔断时间刷新
removalListener:缓存淘汰监听器,配置监听器后,每个条目淘汰时都会调用该监听器
writerwriter监听器其实提供了两个监听,一个是缓存写入或更新是的write,一个是缓存淘汰时的delete,每个条目淘汰时都会调用该监听器

手动填充表示任何数据都需要手动put到cache中,没有任何自动加载策略。put方法会覆盖相同key的条目

  • 同步填充
  • 异步填充
    异步填充于同步填充大致相似,区别是传入一个执行器进行异步执行,并且返回一个CompletableFuture对象,可以通过CompletableFuture.get来获取数据并设置超时时间。

回收策略

条目的自动淘汰回收是map于cache最大的区别,Caffeine同样包含了3中缓存回收机制,分别是基于大小,基于时间,基于引用类型。

  • 基于大小
  • 基于时间
  • 基于引用类型

    自动刷新

    cache除了会自动淘汰缓存数据,也能进行自动刷新缓存数据。
    private static Cache<String, String> cache = Caffeine.newBuilder().expireAfterWrite(10000, TimeUnit.MILLISECONDS).refreshAfterWrite(10000, TimeUnit.MILLISECONDS).build();

    refreshAfterWrite就是设置写入后多就会刷新,expireAfterWrite和refreshAfterWrite的区别是,当缓存过期后,配置了expireAfterWrite,则调用时会阻塞,等待缓存计算完成,返回新的值并进行缓存,refreshAfterWrite则是返回一个旧值,并异步计算新值并缓存。

Caffeine的使用案例代码

/**
* @author vincent.li
* @Description ConfigCache本地缓存类
* @since 2020/9/01
*/
@Component
@Slf4j
public class ConfigCache {

/**
* 自己实现的一个查询配置表数据的服务
*/
@Autowired
private ConfigService configService;

private static Lock lock = new ReentrantLock();

/**
* key过期时间
* 全量刷新没有用invalidateAll(为了时并发线程可以获取到缓存旧值),对于数据库中已删除的key,需要靠过期来使其失效
*
*/
private static final long EXPIRE_TIME = 172800L;

/**
* 上一次字典全量刷新的时间
*/
private static long lastRefreshTime = 0L;
/**
* 全量字典数据刷新时间间隔,需小于单个key过期时间,否则刷新字典时,并发线程可能遇到key过期的情况
*/
private static final long REFRESH_INTERVAL = 86400L;

private static Cache<String, Map<String, String>> configTypeCache = Caffeine.newBuilder().maximumSize(5000).expireAfterWrite(EXPIRE_TIME, TimeUnit.MILLISECONDS).build();
private static Cache<String, String> cache = Caffeine.newBuilder().expireAfterWrite(10000, TimeUnit.MILLISECONDS).refreshAfterWrite(10000, TimeUnit.MILLISECONDS).build();

/**
* 加载全量字典数据到缓存
*/
@PostConstruct
public void reload() {
try {
log.info("start reload ConfigCache cache");
//获取全量字典数据
Map<String, Map<String, String>> configMap = configService.selectConfigItemMap();
configTypeCache.putAll(configMap);

lastRefreshTime = System.currentTimeMillis();
log.info("finish reload ConfigCache cache");
} catch (Exception e) {
log.warn("加载OrderPrintConfig失败", e);
}
}




/**
* 根据type获取configMap<key,value>,会判断是否需要全量刷新缓存
* @param type 类型
* @return Map<String, String>
*/
public static Map<String, String> getByType(String type) {
if (StringUtils.isEmpty(type)) {
return Maps.newHashMap();
}
try {
if (System.currentTimeMillis() - lastRefreshTime > REFRESH_INTERVAL) {
if (lock.tryLock()) {
try {
if (System.currentTimeMillis() - lastRefreshTime > REFRESH_INTERVAL) {
ApplicationContext ctx = SpringBeanUtil.getContext();
if (Objects.nonNull(ctx) && Objects.nonNull(ctx.getBean(OrderPrintConfigCache.class))) {
ConfigCache configCache = ctx.getBean(ConfigCache.class);
configCache.reload();
}
return configTypeCache.getIfPresent(type);
}
} catch (Exception e) {
log.info("configTypeCache数据更新异常", e);
} finally {
lock.unlock();
}
} else {
//获取锁失败,表明另一线程正在更新全量字典,直接从缓存中拿旧值
return configTypeCache.getIfPresent(type);
}
}
return configService.get(type, ConfigCache::loadDictByType);

} catch (Exception e) {
log.info("configTypeCache缓存未命中", e);
return Maps.newHashMap();
}
}



private static Map<String, String> loadDictByType(String type) {
ApplicationContext ctx = SpringBeanUtil.getContext();
if (Objects.nonNull(ctx) && Objects.nonNull(ctx.getBean(ConfigCache.class))) {
ConfigCache dictCache = ctx.getBean(ConfigCache.class);
return dictCache.loadByKey(type);
}
return Maps.newHashMap();
}

private Map<String, String> loadByKey(String type) {
return ConfigService.selectConfigItemMap(type);
}


}