Caffeine Cache解析(一):接口设计与TinyLFU

Caffeine is a high performance Java caching library providing a near optimal hit rate.

  • 自动加载value, 支持异步加载
  • 基于size的eviction:frequency and recency
  • 基于时间的过期策略:last access or last write
  • 异步更新value
  • key支持weak reference
  • value支持weak reference和soft reference
  • 淘汰(或移除)通知
  • 缓存获取数据统计

Caffeine的性能benchmark可参考Caffeine Benchmarks

可以看到Caffeine在不同读写比的情况下, 吞吐量比Guava Cache和Jdk内置的HashMap都有较大提升。

接下来了解下Caffeine的使用、接口设计和TinyLFU简介。

使用

java 复制代码
        // manual cache,Cache-Aside模式
        Cache<String, String> cache = Caffeine.newBuilder()
                                          .expireAfterWrite(Duration.ofHours(1))
                                          .build();
        
        // null表示不存在key
        @Nullable String val = cache.getIfPresent("hello");
        if (val == null) {
            cache.put("hello", "world");
        }
        
        // key不存在则走后面的loading function
        String val2 = cache.get("hello", key -> "world");


        // loading cache,Read-Through模式
        LoadingCache<String, String> loadingCache = Caffeine.newBuilder()
                                                 .expireAfterAccess(Duration.ofMinutes(10))
                                                 .maximumSize(2000)
                                                 .build(new CacheLoader<String, String>() {
                                                     @Override
                                                     public @Nullable String load(@NonNull String s) throws Exception {
                                                         // your loading logic
                                                         return "";
                                                     }
                                                 });
        // 不存在会自动loading
        String val3 = loadingCache.get("hello");

接口设计

CacheCache按不同维度划分:

  • kv是否有限:Bounded or Unbounded,判断参见方法com.github.benmanes.caffeine.cache.Caffeine#isBounded,大部分场景都使用BoundedCache
  • 是否自动loading: ManualCache和LoadingCache
  • 是否异步: Cache和AsyncCache

对应接口UML如下(实现类只画出了BoundedCache),其接口和类均位于com.github.benmanes.caffeine.cache包下:

可以看到,核心的kv存储是放在了LocalCache接口(本质是一个ConcurrentMap)下,而其他的Loading和Async接口在存储基础上附加了自动加载value和异步加载的能力,以同步接口为例,接口能力如下:

  • Cache: 提供缓存基础能力,包含设置、读取、失效、获取缓存统计数据等功能;
  • LoadingCache: 提供自动加载能力,在Cache基础上新增不存在则loading value的读取以及refresh方法;
  • LocalCache: 提供核心存储能力,线程安全和原子能力保证,继承自java.util.concurrent.ConcurrentMap
  • LocalManualCache: 基于LocalCache做存储的非自动Loading Cache;
  • LocalLoadingCache: 继承自LocalManualCache,新增Loading功能。

关于以上接口的一个关键抽象实现类BoundedLocalCache,其作用解释如下:

This class performs a best-effort bounding of a ConcurrentHashMap using a page-replacement algorithm to determine which entries to evict when the capacity is exceeded.

TinyLFU & W-TinyLFU

在看BoundedLocalCache类前先来了解下TinyLFU & W-TinyLFU算法,

算法出自论文:TinyLFU: A Highly Efficient Cache Admission Policy

  • admission policy:控制一个元素是否能进入缓存
  • eviction policy:当缓存满了时选择哪一个元素剔除缓存

论文中提出的TinyLFU指的是admission policy,

TinyLFU可以和 LRU 、SLRU 的eviction policy组合。

比如 论文中使用W-TinyLFU + LRU + SLRU的组合进行实验:

Window Tiny LFU (W-TinyLFU) ... consists of two cache areas.

The main cache employs the SLRU eviction policy and TinyLFU admission policy

while the window cache employs an LRU eviction policy without any admission policy.

可以看到 W-TinyLFU 的main cache area 使用 TinyLFU 作为 admission policy。

TinyLFU 在 LFU的基础上 加了Tiny,而Tiny体现在counter计数(和frequency正相关) 的记录上。

通常大部分缓存的元素访问频率都会很小,如果使用integer来记录counter计数,会导致空间浪费,因此

TinyLFU 借助 approximate counting scheme 来记录counter计数,其原理和bloom filter类似,而 schema的具体实现有:

Caffeine中的CM-Sketch使用4bit记录counter次数,最大只能到15,

并使用long[]类型存储,一个元素可存储16个计数器,long[]即相当于论文中的二维数组。

此外还增加了一个DoorKeeper即一个Bloom Filter来优先存储counter计数为1的元素,大于1的才会进入CM-Sketch结构中。

此外,TinyLFU利用Freshness Mechanism保证TinyLFU计数器记录的次数不会无限制扩大:

Every time we add an item to the approximation sketch, we increment a counter.

Once this counter reaches the sample size (W), we divide it and all other counters in the

approximation sketch by 2.

对应的可以在FrequencySketch类的increment方法中找到reset方法的调用:

java 复制代码
// This class maintains a 4-bit CountMinSketch.
// The maximum frequency of an element is limited to 15 (4-bits) 
// and an aging process periodically halves the popularity of all elements.
final class FrequencySketch<E> {
  ...
  int sampleSize;
  int size;

  public void ensureCapacity(@NonNegative long maximumSize) {
    ...
    sampleSize = (maximumSize == 0) ? 10 : (10 * maximum);
    if (sampleSize <= 0) {
      sampleSize = Integer.MAX_VALUE;
    }
    size = 0;
  }

  // 计数器次数小于15时才会增加计数器
  public void increment(@NonNull E e) {
    ...
    boolean added = ...
    if (added && (++size == sampleSize)) {
      reset();
    }
  }
  ...
}
相关推荐
罗政27 分钟前
PDF书籍《手写调用链监控APM系统-Java版》第12章 结束
java·开发语言·pdf
GraduationDesign31 分钟前
基于SpringBoot的蜗牛兼职网的设计与实现
java·spring boot·后端
今天不学习明天变拉吉40 分钟前
大批量数据导入接口的优化
java·excel
小手cool41 分钟前
取多个集合的交集
java
全栈老实人_44 分钟前
农家乐系统|Java|SSM|VUE| 前后端分离
java·开发语言·tomcat·maven
customer081 小时前
【开源免费】基于SpringBoot+Vue.JS安康旅游网站(JAVA毕业设计)
java·vue.js·spring boot·后端·kafka·开源·旅游
点点滴滴的记录1 小时前
Java的CompletableFuture实现原理
java·开发语言·javascript
xiaolingting1 小时前
Java 引用是4个字节还是8个字节?
java·jvm·引用·指针压缩
一只傻小白,1 小时前
JAVA项目中freemarker静态模板技术
java·开发语言
袁庭新1 小时前
Spring Boot项目接收前端参数的11种方式
java·springboot·袁庭新·如何接收前端数据·boot接收数据