什么是Caffeine Cache
- Caffeine Cache 是一款 JAVA 本地缓存中间件
- Caffeine Cache 可以通过自定义的配置,自动移除“不常用”的数据,以保持内存的合理占用。
- Caffeine Cache 另一个特点是,使用了Window TinyLfu 回收策略,提供了一个近乎最佳的命中率, 该策略优于 LFU、LRU 等算法。
- 从 Spring 2.0 开始,Spring 使用 Caffeine Cache 替代了 Guava Cache。
不同的缓存淘汰算法
- FIFO:先进先出,在这种淘汰算法中,先进入缓存的会先被淘汰,会导致命中率很低。
- LRU:最近最少使用算法,每次访问数据都会将其放在我们的队尾,如果需要淘汰数据,就只需要淘汰队首即可。
- LFU:最近最少频率使用,利用额外的空间记录每个数据的使用频率,然后选出频率最低进行淘汰。
LRU\LFU 算法的局限性
- LRU 可以很好的应对突发流量的情况,因为他不需要累计数据频率。但LRU通过历史数据来预测未来是局限的,它会认为最后到来的数据是最可能被再次访问的,从而给与它最高的优先级。
- LFU 算法局限性 : 假设我们缓存了一个热点新闻,他在初始几天内点击率非常高,在缓存中的命中率也非常高。但一周后热度过去了,该新闻也无人点击访问了,可以由于前期的点击率访问量太高,导致使用 LFU 算法无法将其移除缓存。
TinyLFU 算法
TinyLFU维护了近期访问记录的频率信息,作为一个过滤器,当新记录来时,只有满足TinyLFU要求的记录才可以被插入缓存。
如前所述,作为现代的缓存,它需要解决两个挑战:
- 如何避免维护频率信息的高开销。
- 使用 (Count–Min Sketch)算法解决。
- 如何反应随时间变化的访问模式。
- 解决这个问题是让记录尽量保持相对的“新鲜”(Freshness Mechanism),并且当有新的记录插入时,可以让它跟老的记录进行“PK”,输者就会被淘汰,这样一些老的、不再需要的记录就会被剔除。
统计频率 Count–Min Sketch 算法
Caffeine 对这个算法的实现在
FrequencySketch类。Caffeine 使用了一个一维的数组;如果是数值类型的话,这个数需要用 int 或 long 来存储,但是 Caffeine 认为缓存的访问频率不需要用到那么大,只需要 15 就足够,一般认为达到 15 次的频率算是很高的了,而且 Caffeine 还有另外一个机制来使得这个频率进行衰退减半。如果最大是 15 的话,那么只需要 4 个 bit 就可以满足了,一个 long 有 64bit,可以存储 16 个这样的统计数,使得存储效率提高了 16 倍.
保新机制
为了让缓存保持“新鲜”,剔除掉过往频率很高但之后不经常的缓存,Caffeine 有一个新鲜度机制。就是当整体的统计计数达到某一个值时,那么所有记录的频率统计除以 2。
//size变量就是所有记录的频率统计之,即每个记录加1,这个size都会加1
//sampleSize一个阈值,从FrequencySketch初始化可以看到它的值为maximumSize的10倍
if (added && (++size == sampleSize)) {
reset();
}
void reset() {
int count = 0;
for (int i = 0; i < table.length; i++) {
count += Long.bitCount(table[i] & ONE_MASK);
table[i] = (table[i] >>> 1) & RESET_MASK;
}
size = (size >>> 1) - (count >>> 2);
}
使用方式
基础用法
/**
* 基础用法
*/
@Test
public void basic() throws InterruptedException {
Cache<String, Object> cache = Caffeine.newBuilder()
.initialCapacity(100)//初始大小
.maximumSize(300)//最大数量
.expireAfterWrite(3, TimeUnit.SECONDS)//数据过期时间
.build();
cache.put("testKey", "testValue");
System.out.println("getIfPresent -- " + cache.getIfPresent("testKey"));
TimeUnit.SECONDS.sleep(4);
System.out.println("getIfPresent -- " + cache.getIfPresent("testKey"));
}
输出结果
监听缓存过期事件
/**
* 监听缓存过期事件
*/
@Test
public void removalListener() throws InterruptedException {
Cache<String, Object> cache = Caffeine.newBuilder()
.initialCapacity(1)//初始大小
.maximumSize(3)//最大数量
.expireAfterWrite(3, TimeUnit.SECONDS)//数据过期时间
.removalListener((k, v, c) -> {
System.out.println("Remove:" + k);
})
.build();
cache.put("testKey", "testValue");
System.out.println("getIfPresent -- " + cache.getIfPresent("testKey"));
for (int i = 0; i < 5; i++) {
cache.put("testKey - " + i, "testValue - " + i);
TimeUnit.MILLISECONDS.sleep(1000);
}
System.out.println("getIfPresent -- " + cache.getIfPresent("testKey"));
}
输出结果
根据操作事件,自定义缓存过期时间
public class MyExpireAfter implements Expiry<String, Object> {
@Override
public long expireAfterCreate(@NonNull String key, @NonNull Object value, long currentTime) {
System.out.println(key + " -- expireAfterCreate");
return 100000 * 1000 * 3;
}
@Override
public long expireAfterUpdate(@NonNull String key, @NonNull Object value, long currentTime, @NonNegative long currentDuration) {
System.out.println(key + " -- expireAfterUpdate");
return 100000 * 1000 * 3;
}
@Override
public long expireAfterRead(@NonNull String key, @NonNull Object value, long currentTime, @NonNegative long currentDuration) {
System.out.println(key + " -- expireAfterRead");
return 100000 * 1000 * 3;
}
}
/**
* 根据操作事件,自定义缓存过期时间
*/
@Test
public void expireAfter() {
Cache<String, Object> cache = Caffeine.newBuilder().initialCapacity(1)//初始大小
.maximumSize(3)//最大数量
.expireAfter(new MyExpireAfter())
.build();
System.out.println("--开始测试写入数据");
cache.put("testKey", "testValue");
System.out.println("--开始测试读取数据");
System.out.println("getIfPresent -- " + cache.getIfPresent("testKey"));
System.out.println("--开始测试更新策略,连续写入10个相同的数据");
for (int i = 0; i < 10; i++) {
cache.put("testKey", "testValue");
}
}
输出内容
Caffine Cache 的其他配置
过期策略
Caffeine 为我们提供了三种过期策略 ,分别是:
- 基于大小(size-based)
- 基于时间(time-based)
- 基于引用(reference-based)
@Test
public void expirationStrategy() {
//基于大小(size-based)
//超过容量后淘汰
Caffeine.newBuilder()
.maximumSize(3)
.build();
//基于时间(time-based)
//在最后一次写入缓存后开始计时,在指定的时间后过期。
Caffeine.newBuilder()
.maximumSize(3)
.expireAfterWrite(3, TimeUnit.SECONDS)
.build();
//在最后一次读或者写入后开始计时,在指定的时间后过期。假如一直有请求访问该key,那么这个缓存将一直不会过期。
Caffeine.newBuilder()
.maximumSize(3)
.expireAfterAccess(3, TimeUnit.SECONDS)
.build();
//在expireAfter中需要自己实现Expiry接口
Caffeine.newBuilder()
// 最大容量为1
.maximumSize(1)
.expireAfter(new Expiry<String, String>() {
@Override
public long expireAfterCreate(@NonNull String key, @NonNull String value, long currentTime) {
return currentTime;
}
@Override
public long expireAfterUpdate(@NonNull String key, @NonNull String value, long currentTime, @NonNegative long currentDuration) {
return currentTime;
}
@Override
public long expireAfterRead(@NonNull String key, @NonNull String value, long currentTime, @NonNegative long currentDuration) {
return currentTime;
}
})
.build();
// 基于引用(reference-based)
// 当key和value都没有引用时驱逐缓存
Caffeine.newBuilder()
.weakKeys()
.weakValues()
.build();
// 当垃圾收集器需要释放内存时驱逐
Caffeine.newBuilder()
.softValues()
.build();
}
删除策略
- 单个删除:
Cache.invalidate(key)- 批量删除:
Cache.invalidateAll(keys)- 删除所有缓存项:
Cache.invalidateAll
这是一些常用配置和写法,还有一些更高级的功能,例如:基于注解的缓存配置、异步加载缓存、缓存写入外部存储、统计数据等,会整理后再写一期文章发布。