Caffeine本地缓存之道

话说:Java 8的高性能缓存库,干掉GuavaCache:Caffeine才是本地缓存的王

Caffeine 是基于Java 8的高性能,接近最佳的缓存库。

Caffeine使用Google Guava启发的API提供内存缓存。 改进取决于您设计Guava缓存和ConcurrentLinkedHashMap的体验。

LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
    .maximumSize(10_000)
    .expireAfterWrite(5, TimeUnit.MINUTES)
    .refreshAfterWrite(1, TimeUnit.MINUTES)
    .build(key -> createExpensiveGraph(key));

前言:

官方介绍Caffeine是基于JDK8的高性能本地缓存库,提供了几乎完美的命中率。它有点类似JDK中的ConcurrentMap,实际上,Caffeine中的LocalCache接口就是实现了JDK中的ConcurrentMap接口,但两者并不完全一样。最根本的区别就是,ConcurrentMap保存所有添加的元素,除非显示删除之(比如调用remove方法)。而本地缓存一般会配置自动剔除策略,为了保护应用程序,限制内存占用情况,防止内存溢出。

Caffeine提供了灵活的构造方法,从而创建可以满足如下特性的本地缓存:

  1. 自动把数据加载到本地缓存中,并且可以配置异步;

  2. 基于数量剔除策略;

  3. 基于失效时间剔除策略,这个时间是从最后一次访问或者写入算起;

  4. 异步刷新;

  5. Key会被包装成Weak引用;

  6. Value会被包装成Weak或者Soft引用,从而能被GC掉,而不至于内存泄漏;

  7. 数据剔除提醒;

  8. 写入广播机制;

  9. 缓存访问可以统计;

上压测:

对比如下:

从官方的压测结果来看,无论是全读场景、全写场景、或者读写混合场景,无论是8个线程,还是16个线程,Caffeine都是完胜、碾压,简直就是扛把子届的加特林。

使用:

Caffeine使用还是非常简单的,如果你用过GuavaCache,那就更简单了,因为Caffeine的API设计大量借鉴了GuavaCache。首先,引入Maven依赖:

<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>2.8.4</version>
</dependency>
public static void main(String[] args) {
    Cache<String,String> cache = Caffeine.newBuilder()
        .maximumSize(1024)
        .expireAfterWrite(5, TimeUnit.SECONDS)
        .weakKeys()
        .weakValues()
        .removalListener((RemovalListener<String,String>) (key,value,cause) ->
                System.out.println("key:"+ key + ",value:"+value + ",cause:"+cause.toString()))
        .build();

    //将数据放到本地缓存中
    cache.put("username","caffer");
    cache.put("password","123456");

    //此处可以设置过期时间
    try {
      Thread.sleep(4000L);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    //从本地取出数据
    System.out.println(cache.getIfPresent("username"));
    System.out.println(cache.getIfPresent("password"));
    System.out.println(cache.get("bolog",key -> {
      return "从redis缓存获取";
    }));
  }
AsyncLoadingCache<String, String> cache = Caffeine.newBuilder()
        // 数量上限
        .maximumSize(2)
        // 失效时间
        .expireAfterWrite(5, TimeUnit.MINUTES)
        .refreshAfterWrite(1, TimeUnit.MINUTES)
        // 异步加载机制
        .buildAsync(new CacheLoader<String, String>() {
            @Nullable
            @Override
            public String load(@NonNull String key) throws Exception {
                return getValue(key);
            }
        });
System.out.println(cache.get("username").get());
System.out.println(cache.get("password").get(6, TimeUnit.MINUTES));
System.out.println(cache.get("username").get(6, TimeUnit.MINUTES));
System.out.println(cache.get("blog").get());

过期机制

本地缓存的过期机制是非常重要的,因为本地缓存中的数据并不像业务数据那样需要保证不丢失。本地缓存的数据一般都会要求保证命中率的前提下,尽可能的占用更少的内存,并可在极端情况下,可以被GC掉。

Caffeine的过期机制都是在构造Cache的时候申明,主要有如下几种:

  1. expireAfterWrite:表示自从最后一次写入后多久就会过期;

  2. expireAfterAccess:表示自从最后一次访问(写入或者读取)后多久就会过期;

  3. expireAfter:自定义过期策略;

刷新机制

在构造Cache时通过refreshAfterWrite方法指定刷新周期,例如refreshAfterWrite(10, TimeUnit.SECONDS)表示10秒钟刷新一次:

.build(new CacheLoader<String, String>() {
    @Override
    public String load(String k) {
        // 这里我们就可以从数据库或者其他地方查询最新的数据
        return getValue(k);
    }
});

Tips: Caffeine的刷新机制是「被动」的。举个例子,假如我们申明了10秒刷新一次。我们在时间T访问并获取到值v1,在T+5秒的时候,数据库中这个值已经更新为v2。但是在T+12秒,即已经过了10秒我们通过Caffeine从本地缓存中获取到的「还是v1」,并不是v2。在这个获取过程中,Caffeine发现时间已经过了10秒,然后会将v2加载到本地缓存中,下一次获取时才能拿到v2。即它的实现原理是在get方法中,调用afterRead的时候,调用refreshIfNeeded方法判断是否需要刷新数据。这就意味着,如果不读取本地缓存中的数据的话,无论刷新时间间隔是多少,本地缓存中的数据永远是旧的数据!

剔除机制

在构造Cache时可以通过removalListener方法申明剔除监听器,从而可以跟踪本地缓存中被剔除的数据历史信息。根据RemovalCause.java枚举值可知,剔除策略有如下5种:

  • 「EXPLICIT」:调用方法(例如:cache.invalidate(key)、cache.invalidateAll)显示剔除数据;

  • 「REPLACED」:不是真正被剔除,而是用户调用一些方法(例如:put(),putAll()等)盖了之前的值;

  • 「COLLECTED」:表示缓存中的Key或者Value被垃圾回收掉了;

  • 「EXPIRED」: expireAfterWrite/expireAfterAccess约定时间内没有任何访问导致被剔除;

  • 「SIZE」:超过maximumSize限制的元素个数被剔除的原因;

GuavaCache和Caffeine差异

  1. 剔除算法方面,GuavaCache采用的是「LRU」算法,而Caffeine采用的是「Window TinyLFU」算法,这是两者之间最大,也是根本的区别。

  2. 立即失效方面,Guava会把立即失效 (例如:expireAfterAccess(0) and expireAfterWrite(0)) 转成设置最大Size为0。这就会导致剔除提醒的原因是SIZE而不是EXPIRED。Caffiene能正确识别这种剔除原因。

  3. 取代提醒方面,Guava只要数据被替换,不管什么原因,都会触发剔除监听器。而Caffiene在取代值和先前值的引用完全一样时不会触发监听器。

  4. 异步化方方面,Caffiene的很多工作都是交给线程池去做的(默认:ForkJoinPool.commonPool()),例如:剔除监听器,刷新机制,维护工作等。

内存占用对比

Caffeine可以根据使用情况延迟初始化,或者动态调整它内部数据结构。这样能减少对内存的占用。如下图所示,使用了gradle memoryOverhead对内存占用进行了压测。结果可能会受到JVM的指针压缩、对象Padding等影响:

LRU P.K. W-TinyLFU

缓存的驱逐策略是为了预测哪些数据在短期内最可能被再次用到,从而提升缓存的命中率。由于简洁的实现、高效的运行时表现以及在常规的使用场景下有不错的命中率,LRU(Least Recently Used)策略或许是最流行的驱逐策略,,它在保持算法简单的前提下,效果还不错。但LRU对未来的预测有明显的局限性,它会认为「最后到来的数据是最可能被再次访问」的,从而给予它最高的优先级。

现代缓存扩展了对历史数据的使用,结合就近程度(recency)和访问频次(frequency)来更好的预测数据。

Guava迁移

那么,如果我的项目之前用的是GuavaCache,如何以尽可能低的成本迁移到Caffeine上来呢?嘿嘿,Caffeine已经想到了这一点,它提供了一个适配器,让你用Guava的接口操作它的缓存。代码片段如下所示:

// Guava's LoadingCache interface
LoadingCache<Key, Graph> graphs = CaffeinatedGuava.build(
    Caffeine.newBuilder().maximumSize(10_000),
    new CacheLoader<Key, Graph>() { // Guava's CacheLoader
        @Override public Graph load(Key key) throws Exception {
          return createExpensiveGraph(key);
        }
    });

实战:

填充策略(Population)

Caffeine 为我们提供了三种填充策略:手动、同步和异步

驱逐策略(eviction)

Caffeine提供三类驱逐策略:基于大小(size-based),基于时间(time-based)和基于引用(reference-based)。

基于引用:

强引用,软引用,弱引用概念说明请点击连接,这里说一下各各引用的区别:

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

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

移除监听器(Removal)

概念:

  • 驱逐(eviction):由于满足了某种驱逐策略,后台自动进行的删除操作
  • 无效(invalidation):表示由调用方手动删除缓存
  • 移除(removal):监听驱逐或无效操作的监听器

刷新(Refresh)

LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
    .maximumSize(10_000)
    // 指定在创建缓存或者最近一次更新缓存后经过固定的时间间隔,刷新缓存
    .refreshAfterWrite(1, TimeUnit.MINUTES)
    .build(key -> createExpensiveGraph(key));

刷新和驱逐是不一样的。

刷新的是通过LoadingCache.refresh(key)方法来指定,并通过调用CacheLoader.reload方法来执行,刷新key会异步地为这个key加载新的value,并返回旧的值(如果有的话)。驱逐会阻塞查询操作直到驱逐作完成才会进行其他操作。

与expireAfterWrite不同的是,refreshAfterWrite将在查询数据的时候判断该数据是不是符合查询条件,如果符合条件该缓存就会去执行刷新操作。例如,您可以在同一个缓存中同时指定refreshAfterWrite和expireAfterWrite,只有当数据具备刷新条件的时候才会去刷新数据,不会盲目去执行刷新操作。

如果数据在刷新后就一直没有被再次查询,那么该数据也会过期。

刷新操作是使用Executor异步执行的。默认执行程序是ForkJoinPool.commonPool(),可以通过Caffeine.executor(Executor)覆盖。

如果刷新时引发异常,则使用log记录日志,并不会抛出。

统计(Statistics)

Cache<Key, Graph> graphs = Caffeine.newBuilder()
    .maximumSize(10_000)
    .recordStats()
    .build();

使用Caffeine.recordStats(),您可以打开统计信息收集。Cache.stats() 方法返回提供统计信息的CacheStats,如:

  • hitRate():返回命中与请求的比率
  • hitCount(): 返回命中缓存的总数
  • evictionCount():缓存逐出的数量
  • averageLoadPenalty():加载新值所花费的平均时间

TIPS:

  • expireAfterAccess(long, TimeUnit): 最后一次被访问(读或者写)后多久失效
  • expireAfterWrite(long, TimeUnit): 最后一次被创建或修改后多久失效
  • expireAfter(Expiry): 创建后多久失效 

参考文档:

干掉GuavaCacheCaffeine才是本地缓存的王: 干掉GuavaCache:Caffeine才是本地缓存的王_朱小厮的博客-CSDN博客

caffeine源码: https://github.com/ben-manes/caffeine 

Caffeine缓存Caffeine缓存 - xiaolyuh的个人空间 - OSCHINA - 中文开源技术交流社区

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Caffeine本地缓存适用于以下场景: 1. 提升应用性能:Caffeine本地缓存可以将频繁访问的数据缓存在内存中,以减少对底层数据源的访问次数,从而提高应用的性能和响应速度。 2. 降低系统负载:通过使用Caffeine本地缓存,可以避免频繁地从数据库或其他外部数据源中读取数据,减少了对外部系统的访问,从而降低了系统的负载。 3. 数据共享与共享状态管理:Caffeine本地缓存可以用于在应用内部共享数据,减少重复计算和查询的开销。同时,它也可以用于管理应用中的共享状态,确保多个线程或进程之间的数据一致性。 4. 缓存数据的有效期管理:Caffeine本地缓存提供了对缓存数据有效期的管理,可以根据需求设置缓存过期时间,以确保缓存数据的及时更新和一致性。 5. 缓解外部服务的压力:对于一些需要频繁调用外部服务的场景,可以使用Caffeine本地缓存缓存外部服务的响应结果,减少对外部服务的调用次数,降低对外部服务的压力。 总之,Caffeine本地缓存适用于需要提升应用性能、降低系统负载、数据共享与共享状态管理、缓存数据有效期管理和缓解外部服务压力的场景。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [Caffeine一级缓存介绍和应用](https://blog.csdn.net/u011507134/article/details/127107322)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值