0%

Caffeine

概述:

Caffeine是一个基于Java8和Guava Cache重写的高性能的JVM缓存工具。

官方文档地址:https://github.com/ben-manes/caffeine

效率:

Caffeine使用了W-TinyLFU淘汰算法,使缓存命中率提升至接近最佳,同时占用的内存尽可能少。

不同类型的实现:

  • Cache - 手动填充缓存:
    • 需要显式的去控制缓存的创建,更新和删除。
  • LoadingCache - 同步加载缓存:
    • 使用CacheLoader来构建的缓存的值。批量查找可以使用Caffeine.getAll(Iterable<? extends String>)方法。默认情况下,getAll将会对缓存中没有值的key分别调用CacheLoader.load方法来构建缓存的值。我们可以重写CacheLoader.loadAll方法来提高getAll的效率。
    • 如果get一个不存在的key,会调用定义好的load方法,加载一个默认值。可有效应对恶意调用不存在的key的攻击行为,防止缓存击穿。
  • AsyncLoadingCache - 异步加载缓存:
  • AsyncLoadingCache是继承自LoadingCache类的,异步加载使用Executor去调用方法并返回一个CompletableFuture。异步加载缓存使用了响应式编程模型。

过期策略:

  • size - 基于容量/大小

    • Caffeine.maximumSize(long),例 long = 1000 表示最有同时存储1000个Entry,也就是缓存1000个K-V值。`
    • Caffeine.estimatedSize()返回当前已经占用的Entry数。基于权重(比较难理解,我也还没理解)
    • Caffeine.maximumWeight(long)。CacheBuilder.weigher(Weigher),可以指定权重函数。通过权重函数计算出当前对‘总重’。如果‘总重’超过限制,就淘汰缓存。
  • time - 基于时间

    • Caffeine.expireAfterAccess(long, TimeUnit) 缓存项在给定时间内没有被‘读/写’访问,则回收。

    • Caffeine.expireAfterWrite(long, TimeUnit) 缓存项在给定时间内没有被写访问(创建或覆盖),则回收。

    • Caffeine.expireAfter(Expiry) 可以在Expiry中分别自定义 读/写/创建等操作的超时时间,比上面两种策略更加灵活。

  • reference - 基于引用

    • Caffeine.weakKeys()使用弱引用存储键。当键没有其它(强或软)引用时,缓存项可以被垃圾回收。因为垃圾回收仅依赖恒等式(==),使用弱引用键的缓存用==而不是equals比较键。

    • Caffeine.weakValues()使用弱引用存储值。当值没有其它(强或软)引用时,缓存项可以被垃圾回收。因为垃圾回收仅依赖恒等式(==),使用弱引用值的缓存用==而不是equals比较值。

    • Caffeine.softValues()使用软引用存储值。软引用只有在响应内存需要时,才按照全局最近最少使用的顺序回收。考虑到使用软引用的性能影响,建议使用更有性能预测性的缓存大小限定。使用软引用值的缓存同样用==而不是equals比较值。

Java4种引用的级别由高到低依次为:强引用 > 软引用 > 弱引用 > 虚引用

引用类型 被垃圾回收时间 用途 生存时间
强引用 从来不会 对象的一般状态 JVM停止运行时终止
软引用 在内存不足时 对象缓存 内存不足时终止
弱引用 在垃圾回收时 对象缓存 gc运行后终止
虚引用 - - -

移除

  • 一些术语
    • eviction - 驱逐,因为过期策略而删除
    • invalidation - 失效,由用户手动触发而删除
    • removal - 移除结果,发生eviction或者invalidation产生的通知结果
  • ​ 任何时候,你都可以显式地清除缓存项:
    • 单个清除:Cache.invalidate(key)
    • 批量清除:Cache.invalidateAll(keys)
    • 清除所有缓存项:Cache.invalidateAll()
  • 监听器
    • 通过CacheBuilder.removalListener(RemovalListener),你可以声明一个监听器,以便缓存项被移除时做一些额外操作。缓存项被移除时,RemovalListener会获取移除通知(RemovalNotification),其中包含移除原因(RemovalCause)、键和值。
    • 监听器的操作是异步的,由executor执行。executor默认实现是ForkJoinPool.commonPool()。也可以通过Caffeine.executor(Executor)设置想要的executor。

数据统计

  • 通过Caffeine.recordStats()可以开启数据统计。
  • 调用Cache.stats()可以拿到CacheStats数据对象,包含:
  • hitRate() - 命中率
    • evictionCount() - 移除的缓存数量
    • averageLoadPenalty() - 加载新缓存的平均时间
    • ……等
  • 统计的数据也可以通过被动推送的方式拿到,这里不过多描述,有兴趣可以自己研究。

其他特性

  • Tiker
  • 自定义时钟,可以不使用系统默认的时钟,通过自定义的时钟控制缓存的存活时间。
  • refresh
  • LoadingCache的特性,刷新缓存,刷新与删除再写入的不同点在于:刷新为非阻塞操作,在并发的情况下,其他线程可以在刷新的过程中取到旧值。而eviction则会阻塞线程保证数据一致性。
  • AsMap
  • Caffeine可以转成ConcurrentMap进行操作。转换后的map和cache共享数据,所以更新操作会相互影响。
  • 在Spring框架中使用Caffeine

DEMO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* 手动操作型cache
*/
Cache<String,Object> cache = Caffeine.newBuilder()
.maximumSize(2) // 最多两个Entry
.expireAfterWrite(Duration.ofSeconds(5)) // 5s不被使用自动过期
.recordStats() // 开启调用统计
.build();

cache.put("key1","val1");
cache.put("key2","val2");

System.out.println(cache.estimatedSize()); // 2
cache.getIfPresent("key1");

cache.put("key3","val3"); // 此时容量超过2,会淘汰使用频次较低的key2
System.out.println(cache.getIfPresent("key3")); // val3
System.out.println(cache.getIfPresent("key1")); // val1
System.out.println(cache.getIfPresent("key2")); // null


cache.invalidate("key1");
System.out.println(cache.getIfPresent("key1")); // null
//get操作可同时传入一个Callable,数据若不存在会调用Callable计算并存入缓存
System.out.println(cache.get("key1",key -> "val1")); //val1

cache.invalidateAll();
System.out.println(cache.stats().hitRate()); // 0.5 get6次 命中3次
System.out.println(cache.stats().evictionCount()); // 1
System.out.println(cache.estimatedSize()); // 0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
Logger logger = LoggerFactory.getLogger("ClassName");
/**
* 自动加载型cache
*/
LoadingCache<String,Object> loadingCache = Caffeine.newBuilder()
.maximumSize(50)
.expireAfterWrite(1, TimeUnit.MINUTES)
.recordStats()
.removalListener((String key,Object obj, RemovalCause cause) -> {
logger.info("key:{},val:{},cause:{}",key,obj,cause);
System.out.printf("Key %s was removed (%s)%n", key, cause);
})
// 注入load方法
.build(key -> "default");

// 由于key不存在 生成了值为default的缓存
System.out.println(loadingCache.get("keyNotExist"));// default

Map<String,Object> map = Maps.newHashMap();
map.put("key1","val1");
map.put("key2","val2");
map.put("key3","val3");

loadingCache.putAll(map);
System.out.println(loadingCache.get("key1"));// val1

Set<String> keySet = Sets.newHashSet("key5","key6","key7");
loadingCache.getAll(keySet);
System.out.println(loadingCache.get("key5"));// default

ConcurrentMap<String,Object> map1 = loadingCache.asMap();
for (Map.Entry entry : map1.entrySet()) {
// key1:val1|key2:val2|key5:default|key6:default|key3:val3|key7:default|keyNotExist:default
System.out.println(entry.getKey() + ":" + entry.getValue());
}

map1.remove("key2");
System.out.println(loadingCache.getIfPresent("key2")); // null 通过map操作也会影响loadingCache

loadingCache.invalidate("key1");
System.out.println(loadingCache.estimatedSize()); // 6