精华内容
下载资源
问答
  • Caffeine

    2019-10-25 00:51:52
    Caffeine
  • Caffeine 缓存

    万次阅读 2017-12-13 17:07:05
    在本文中,我们来看看Caffeine— 一个高性能的 Java 缓存库。 缓存和 Map 之间的一个根本区别在于缓存可以回收存储的 item。 回收策略为在指定时间删除哪些对象。此策略直接影响缓存的命中率 — 缓存库的一个重要...

    简介

    在本文中,我们来看看 Caffeine — 一个高性能的 Java 缓存库。

    缓存和 Map 之间的一个根本区别在于缓存可以回收存储的 item。

    回收策略为在指定时间删除哪些对象。此策略直接影响缓存的命中率 — 缓存库的一个重要特征。

    Caffeine 因使用 Window TinyLfu 回收策略,提供了一个近乎最佳的命中率。

    填充策略(Population)

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

    手动加载(Manual)

    
    Cache<String, Object> manualCache = Caffeine.newBuilder()
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .maximumSize(10_000)
            .build();
    
    String key = "name1";
    // 根据key查询一个缓存,如果没有返回NULL
    graph = manualCache.getIfPresent(key);
    // 根据Key查询一个缓存,如果没有调用createExpensiveGraph方法,并将返回值保存到缓存。
    // 如果该方法返回Null则manualCache.get返回null,如果该方法抛出异常则manualCache.get抛出异常
    graph = manualCache.get(key, k -> createExpensiveGraph(k));
    // 将一个值放入缓存,如果以前有值就覆盖以前的值
    manualCache.put(key, graph);
    // 删除一个缓存
    manualCache.invalidate(key);
    
    ConcurrentMap<String, Object> map = manualCache.asMap();
    cache.invalidate(key);
    

    Cache接口允许显式的去控制缓存的检索,更新和删除。

    我们可以通过cache.getIfPresent(key) 方法来获取一个key的值,通过cache.put(key, value)方法显示的将数控放入缓存,但是这样子会覆盖缓原来key的数据。更加建议使用cache.get(key,k - > value) 的方式,get 方法将一个参数为 key 的 Function (createExpensiveGraph) 作为参数传入。如果缓存中不存在该键,则调用这个 Function 函数,并将返回值作为该缓存的值插入缓存中。get 方法是以阻塞方式执行调用,即使多个线程同时请求该值也只会调用一次Function方法。这样可以避免与其他线程的写入竞争,这也是为什么使用 get 优于 getIfPresent 的原因。

    注意:如果调用该方法返回NULL(如上面的 createExpensiveGraph 方法),则cache.get返回null,如果调用该方法抛出异常,则get方法也会抛出异常。

    可以使用Cache.asMap() 方法获取ConcurrentMap进而对缓存进行一些更改。

    同步加载(Loading)

    
    LoadingCache<String, Object> loadingCache = Caffeine.newBuilder()
            .maximumSize(10_000)
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .build(key -> createExpensiveGraph(key));
        
    String key = "name1";
    // 采用同步方式去获取一个缓存和上面的手动方式是一个原理。在build Cache的时候会提供一个createExpensiveGraph函数。
    // 查询并在缺失的情况下使用同步的方式来构建一个缓存
    Object graph = loadingCache.get(key);
    
    // 获取组key的值返回一个Map
    List<String> keys = new ArrayList<>();
    keys.add(key);
    Map<String, Object> graphs = loadingCache.getAll(keys);
    

    LoadingCache是使用CacheLoader来构建的缓存的值。

    批量查找可以使用getAll方法。默认情况下,getAll将会对缓存中没有值的key分别调用CacheLoader.load方法来构建缓存的值。我们可以重写CacheLoader.loadAll方法来提高getAll的效率。

    注意:您可以编写一个CacheLoader.loadAll来实现为特别请求的key加载值。例如,如果计算某个组中的任何键的值将为该组中的所有键提供值,则loadAll可能会同时加载该组的其余部分。

    异步加载(Asynchronously Loading)

    
    AsyncLoadingCache<String, Object> asyncLoadingCache = Caffeine.newBuilder()
                .maximumSize(10_000)
                .expireAfterWrite(10, TimeUnit.MINUTES)
                // Either: Build with a synchronous computation that is wrapped as asynchronous
                .buildAsync(key -> createExpensiveGraph(key));
                // Or: Build with a asynchronous computation that returns a future
                // .buildAsync((key, executor) -> createExpensiveGraphAsync(key, executor));
    
     String key = "name1";
    
    // 查询并在缺失的情况下使用异步的方式来构建缓存
    CompletableFuture<Object> graph = asyncLoadingCache.get(key);
    // 查询一组缓存并在缺失的情况下使用异步的方式来构建缓存
    List<String> keys = new ArrayList<>();
    keys.add(key);
    CompletableFuture<Map<String, Object>> graphs = asyncLoadingCache.getAll(keys);
    // 异步转同步
    loadingCache = asyncLoadingCache.synchronous();
    

    AsyncLoadingCache是继承自LoadingCache类的,异步加载使用Executor去调用方法并返回一个CompletableFuture。异步加载缓存使用了响应式编程模型。

    如果要以同步方式调用时,应提供CacheLoader。要以异步表示时,应该提供一个AsyncCacheLoader,并返回一个CompletableFuture。

    synchronous()这个方法返回了一个LoadingCacheView视图,LoadingCacheView也继承自LoadingCache。调用该方法后就相当于你将一个异步加载的缓存AsyncLoadingCache转换成了一个同步加载的缓存LoadingCache。

    默认使用ForkJoinPool.commonPool()来执行异步线程,但是我们可以通过Caffeine.executor(Executor) 方法来替换线程池。

    驱逐策略(eviction)

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

    基于大小(size-based)

    基于大小驱逐,有两种方式:一种是基于缓存大小,一种是基于权重。

    // Evict based on the number of entries in the cache
    // 根据缓存的计数进行驱逐
    LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
        .maximumSize(10_000)
        .build(key -> createExpensiveGraph(key));
    
    // Evict based on the number of vertices in the cache
    // 根据缓存的权重来进行驱逐(权重只是用于确定缓存大小,不会用于决定该缓存是否被驱逐)
    LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
        .maximumWeight(10_000)
        .weigher((Key key, Graph graph) -> graph.vertices().size())
        .build(key -> createExpensiveGraph(key));
    

    我们可以使用Caffeine.maximumSize(long)方法来指定缓存的最大容量。当缓存超出这个容量的时候,会使用Window TinyLfu策略来删除缓存。

    我们也可以使用权重的策略来进行驱逐,可以使用Caffeine.weigher(Weigher) 函数来指定权重,使用Caffeine.maximumWeight(long) 函数来指定缓存最大权重值。

    maximumWeight与maximumSize不可以同时使用。

    基于时间(Time-based)

    
    // Evict based on a fixed expiration policy
    // 基于固定的到期策略进行退出
    LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
        .expireAfterAccess(5, TimeUnit.MINUTES)
        .build(key -> createExpensiveGraph(key));
    LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
        .expireAfterWrite(10, TimeUnit.MINUTES)
        .build(key -> createExpensiveGraph(key));
    
    // Evict based on a varying expiration policy
    // 基于不同的到期策略进行退出
    LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
        .expireAfter(new Expiry<Key, Graph>() {
          @Override
          public long expireAfterCreate(Key key, Graph graph, long currentTime) {
            // Use wall clock time, rather than nanotime, if from an external resource
            long seconds = graph.creationDate().plusHours(5)
                .minus(System.currentTimeMillis(), MILLIS)
                .toEpochSecond();
            return TimeUnit.SECONDS.toNanos(seconds);
          }
          
          @Override
          public long expireAfterUpdate(Key key, Graph graph, 
              long currentTime, long currentDuration) {
            return currentDuration;
          }
          
          @Override
          public long expireAfterRead(Key key, Graph graph,
              long currentTime, long currentDuration) {
            return currentDuration;
          }
        })
        .build(key -> createExpensiveGraph(key));
    

    Caffeine提供了三种定时驱逐策略:

    • expireAfterAccess(long, TimeUnit):在最后一次访问或者写入后开始计时,在指定的时间后过期。假如一直有请求访问该key,那么这个缓存将一直不会过期。
    • expireAfterWrite(long, TimeUnit): 在最后一次写入缓存后开始计时,在指定的时间后过期。
    • expireAfter(Expiry): 自定义策略,过期时间由Expiry实现独自计算。

    缓存的删除策略使用的是惰性删除和定时删除。这两个删除策略的时间复杂度都是O(1)。

    测试定时驱逐不需要等到时间结束。我们可以使用Ticker接口和Caffeine.ticker(Ticker)方法在缓存生成器中指定时间源,而不必等待系统时钟。如:

    
    FakeTicker ticker = new FakeTicker(); // Guava's testlib
    Cache<Key, Graph> cache = Caffeine.newBuilder()
        .expireAfterWrite(10, TimeUnit.MINUTES)
        .executor(Runnable::run)
        .ticker(ticker::read)
        .maximumSize(10)
        .build();
    
    cache.put(key, graph);
    ticker.advance(30, TimeUnit.MINUTES)
    assertThat(cache.getIfPresent(key), is(nullValue());
    

    基于引用(reference-based)

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

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

    引用类型 被垃圾回收时间 用途 生存时间
    强引用 从来不会 对象的一般状态 JVM停止运行时终止
    软引用 在内存不足时 对象缓存 内存不足时终止
    弱引用 在垃圾回收时 对象缓存 gc运行后终止
    虚引用 Unknown Unknown Unknown
    
    // Evict when neither the key nor value are strongly reachable
    // 当key和value都没有引用时驱逐缓存
    LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
        .weakKeys()
        .weakValues()
        .build(key -> createExpensiveGraph(key));
    
    // Evict when the garbage collector needs to free memory
    // 当垃圾收集器需要释放内存时驱逐
    LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
        .softValues()
        .build(key -> createExpensiveGraph(key));
    

    我们可以将缓存的驱逐配置成基于垃圾回收器。为此,我们可以将key 和 value 配置为弱引用或只将值配置成软引用。

    注意:AsyncLoadingCache不支持弱引用和软引用。

    Caffeine.weakKeys() 使用弱引用存储key。如果没有其他地方对该key有强引用,那么该缓存就会被垃圾回收器回收。由于垃圾回收器只依赖于身份(identity)相等,因此这会导致整个缓存使用身份 (==) 相等来比较 key,而不是使用 equals()。

    Caffeine.weakValues() 使用弱引用存储value。如果没有其他地方对该value有强引用,那么该缓存就会被垃圾回收器回收。由于垃圾回收器只依赖于身份(identity)相等,因此这会导致整个缓存使用身份 (==) 相等来比较 key,而不是使用 equals()。

    Caffeine.softValues() 使用软引用存储value。当内存满了过后,软引用的对象以将使用最近最少使用(least-recently-used ) 的方式进行垃圾回收。由于使用软引用是需要等到内存满了才进行回收,所以我们通常建议给缓存配置一个使用内存的最大值。 softValues() 将使用身份相等(identity) (==) 而不是equals() 来比较值。

    注意:Caffeine.weakValues()和Caffeine.softValues()不可以一起使用。

    移除监听器(Removal)

    概念:

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

    手动删除缓存:

    在任何时候,您都可能明确地使缓存无效,而不用等待缓存被驱逐。

    
    // individual key
    cache.invalidate(key)
    // bulk keys
    cache.invalidateAll(keys)
    // all keys
    cache.invalidateAll()
    

    Removal 监听器:

    
    Cache<Key, Graph> graphs = Caffeine.newBuilder()
        .removalListener((Key key, Graph graph, RemovalCause cause) ->
            System.out.printf("Key %s was removed (%s)%n", key, cause))
        .build();
    

    您可以通过Caffeine.removalListener(RemovalListener) 为缓存指定一个删除侦听器,以便在删除数据时执行某些操作。 RemovalListener可以获取到key、value和RemovalCause(删除的原因)。

    删除侦听器的里面的操作是使用Executor来异步执行的。默认执行程序是ForkJoinPool.commonPool(),可以通过Caffeine.executor(Executor)覆盖。当操作必须与删除同步执行时,请改为使用CacheWrite,CacheWrite将在下面说明。

    注意:由RemovalListener抛出的任何异常都会被记录(使用Logger)并不会抛出。

    刷新(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记录日志,并不会抛出。

    Writer

    
    LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
      .writer(new CacheWriter<Key, Graph>() {
        @Override public void write(Key key, Graph graph) {
          // write to storage or secondary cache
        }
        @Override public void delete(Key key, Graph graph, RemovalCause cause) {
          // delete from storage or secondary cache
        }
      })
      .build(key -> createExpensiveGraph(key));
    

    CacheWriter允许缓存充当一个底层资源的代理,当与CacheLoader结合使用时,所有对缓存的读写操作都可以通过Writer进行传递。Writer可以把操作缓存和操作外部资源扩展成一个同步的原子性操作。并且在缓存写入完成之前,它将会阻塞后续的更新缓存操作,但是读取(get)将直接返回原有的值。如果写入程序失败,那么原有的key和value的映射将保持不变,如果出现异常将直接抛给调用者。

    CacheWriter可以同步的监听到缓存的创建、变更和删除操作。加载(例如,LoadingCache.get)、重新加载(例如,LoadingCache.refresh)和计算(例如Map.computeIfPresent)的操作不被CacheWriter监听到。

    注意:CacheWriter不能与weakKeys或AsyncLoadingCache结合使用。

    可能的用例(Possible Use-Cases)

    CacheWriter是复杂工作流的扩展点,需要外部资源来观察给定Key的变化顺序。这些用法Caffeine是支持的,但不是本地内置。

    写模式(Write Modes)

    CacheWriter可以用来实现一个直接写(write-through )或回写(write-back )缓存的操作。

    write-through式缓存中,写操作是一个同步的过程,只有写成功了才会去更新缓存。这避免了同时去更新资源和缓存的条件竞争。

    write-back式缓存中,对外部资源的操作是在缓存更新后异步执行的。这样可以提高写入的吞吐量,避免数据不一致的风险,比如如果写入失败,则在缓存中保留无效的状态。这种方法可能有助于延迟写操作,直到指定的时间,限制写速率或批写操作。

    通过对write-back进行扩展,我们可以实现以下特性:

    • 批处理和合并操作
    • 延迟操作并到一个特定的时间执行
    • 如果超过阈值大小,则在定期刷新之前执行批处理
    • 如果操作尚未刷新,则从写入后缓冲器(write-behind)加载
    • 根据外部资源的特点,处理重审,速率限制和并发

    可以参考一个简单的例子,使用RxJava实现。

    分层(Layering)

    CacheWriter可能用来集成多个缓存进而实现多级缓存。

    多级缓存的加载和写入可以使用系统外部高速缓存。这允许缓存使用一个小并且快速的缓存去调用一个大的并且速度相对慢一点的缓存。典型的off-heap、file-based和remote 缓存。

    受害者缓存(Victim Cache)是一个多级缓存的变体,其中被删除的数据被写入二级缓存。这个delete(K, V, RemovalCause) 方法允许检查为什么该数据被删除,并作出相应的操作。

    同步监听器(Synchronous Listeners)

    同步监听器会接收一个key在缓存中的进行了那些操作的通知。监听器可以阻止缓存操作,也可以将事件排队以异步的方式执行。这种类型的监听器最常用于复制或构建分布式缓存。

    统计(Statistics)

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

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

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

    Cleanup

    缓存的删除策略使用的是惰性删除和定时删除,但是我也可以自己调用cache.cleanUp()方法手动触发一次回收操作。cache.cleanUp()是一个同步方法。

    策略(Policy)

    在创建缓存的时候,缓存的策略就指定好了。但是我们可以在运行时可以获得和修改该策略。这些策略可以通过一些选项来获得,以此来确定缓存是否支持该功能。

    Size-based

    
    cache.policy().eviction().ifPresent(eviction -> {
      eviction.setMaximum(2 * eviction.getMaximum());
    });
    

    如果缓存配置的时基于权重来驱逐,那么我们可以使用weightedSize() 来获取当前权重。这与获取缓存中的记录数的Cache.estimatedSize() 方法有所不同。

    缓存的最大值(maximum)或最大权重(weight)可以通过getMaximum()方法来读取,并使用setMaximum(long)进行调整。当缓存量达到新的阀值的时候缓存才会去驱逐缓存。

    如果有需用我们可以通过hottest(int) 和 coldest(int)方法来获取最有可能命中的数据和最有可能驱逐的数据快照。

    Time-based

    
    cache.policy().expireAfterAccess().ifPresent(expiration -> ...);
    cache.policy().expireAfterWrite().ifPresent(expiration -> ...);
    cache.policy().expireVariably().ifPresent(expiration -> ...);
    cache.policy().refreshAfterWrite().ifPresent(expiration -> ...);
    

    ageOf(key,TimeUnit) 提供了从expireAfterAccess,expireAfterWrite或refreshAfterWrite策略的角度来看条目已经空闲的时间。最大持续时间可以从getExpiresAfter(TimeUnit)读取,并使用setExpiresAfter(long,TimeUnit)进行调整。

    如果有需用我们可以通过hottest(int) 和 coldest(int)方法来获取最有可能命中的数据和最有可能驱逐的数据快照。

    测试(Testing)

    
    FakeTicker ticker = new FakeTicker(); // Guava's testlib
    Cache<Key, Graph> cache = Caffeine.newBuilder()
        .expireAfterWrite(10, TimeUnit.MINUTES)
        .executor(Runnable::run)
        .ticker(ticker::read)
        .maximumSize(10)
        .build();
    
    cache.put(key, graph);
    ticker.advance(30, TimeUnit.MINUTES)
    assertThat(cache.getIfPresent(key), is(nullValue());
    

    测试的时候我们可以使用Caffeine..ticker(ticker)来指定一个时间源,并不需要等到key过期。

    FakeTicker这个是guawa test包里面的Ticker,主要用于测试。依赖:

    <dependency>
        <groupId>com.google.guava</groupId>
        <artifactId>guava-testlib</artifactId>
        <version>23.5-jre</version>
    </dependency>
    

    常见问题(Faq)

    固定数据(Pinning Entries)

    固定数据是不能通过驱逐策略去将数据删除的。当数据是一个有状态的资源时(如锁),那么这条数据是非常有用的,你有在客端使用完这个条数据的时候才能删除该数据。在这种情况下如果驱逐策略将这个条数据删掉的话,将导致资源泄露。

    通过使用权重将该数据的权重设置成0,并且这个条数据不计入maximum size里面。 当缓存达到maximum size 了以后,驱逐策略也会跳过该条数据,不会进行删除操作。我们还必须自定义一个标准来判断这个数据是否属于固定数据。

    通过使用Long.MAX_VALUE(大约300年)的值作为key的有效时间,这样可以将一条数据从过期中排除。自定义到期必须定义,这可以评估条目是否固定。

    将数据写入缓存时我们要指定该数据的权重和到期时间。这可以通过使用cache.asMap()获取缓存列表后,再来实现引脚和解除绑定。

    递归调用(Recursive Computations)

    在原子操作内执行的加载,计算或回调可能不会写入缓存。 ConcurrentHashMap不允许这些递归写操作,因为这将有可能导致活锁(Java 8)或IllegalStateException(Java 9)。

    解决方法是异步执行这些操作,例如使用AsyncLoadingCache。在异步这种情况下映射已经建立,value是一个CompletableFuture,并且这些操作是在缓存的原子范围之外执行的。但是,如果发生无序的依赖链,这也有可能导致死锁。

    示例代码:

    
    package com.xiaolyuh.controller;
    
    import com.alibaba.fastjson.JSON;
    import com.github.benmanes.caffeine.cache.*;
    import com.google.common.testing.FakeTicker;
    import com.xiaolyuh.entity.Person;
    import com.xiaolyuh.service.PersonService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Map;
    import java.util.OptionalLong;
    import java.util.concurrent.CompletableFuture;
    import java.util.concurrent.ConcurrentMap;
    import java.util.concurrent.Executor;
    import java.util.concurrent.TimeUnit;
    
    @RestController
    public class CaffeineCacheController {
    
        @Autowired
        PersonService personService;
    
        Cache<String, Object> manualCache = Caffeine.newBuilder()
                .expireAfterWrite(10, TimeUnit.MINUTES)
                .maximumSize(10_000)
                .build();
    
        LoadingCache<String, Object> loadingCache = Caffeine.newBuilder()
                .maximumSize(10_000)
                .expireAfterWrite(10, TimeUnit.MINUTES)
                .build(key -> createExpensiveGraph(key));
    
        AsyncLoadingCache<String, Object> asyncLoadingCache = Caffeine.newBuilder()
                .maximumSize(10_000)
                .expireAfterWrite(10, TimeUnit.MINUTES)
                // Either: Build with a synchronous computation that is wrapped as asynchronous
                .buildAsync(key -> createExpensiveGraph(key));
        // Or: Build with a asynchronous computation that returns a future
        // .buildAsync((key, executor) -> createExpensiveGraphAsync(key, executor));
    
        private CompletableFuture<Object> createExpensiveGraphAsync(String key, Executor executor) {
            CompletableFuture<Object> objectCompletableFuture = new CompletableFuture<>();
            return objectCompletableFuture;
        }
    
        private Object createExpensiveGraph(String key) {
            System.out.println("缓存不存在或过期,调用了createExpensiveGraph方法获取缓存key的值");
            if (key.equals("name")) {
                throw new RuntimeException("调用了该方法获取缓存key的值的时候出现异常");
            }
            return personService.findOne1();
        }
    
        @RequestMapping("/testManual")
        public Object testManual(Person person) {
            String key = "name1";
            Object graph = null;
    
            // 根据key查询一个缓存,如果没有返回NULL
            graph = manualCache.getIfPresent(key);
            // 根据Key查询一个缓存,如果没有调用createExpensiveGraph方法,并将返回值保存到缓存。
            // 如果该方法返回Null则manualCache.get返回null,如果该方法抛出异常则manualCache.get抛出异常
            graph = manualCache.get(key, k -> createExpensiveGraph(k));
            // 将一个值放入缓存,如果以前有值就覆盖以前的值
            manualCache.put(key, graph);
            // 删除一个缓存
            manualCache.invalidate(key);
    
            ConcurrentMap<String, Object> map = manualCache.asMap();
            System.out.println(map.toString());
            return graph;
        }
    
        @RequestMapping("/testLoading")
        public Object testLoading(Person person) {
            String key = "name1";
    
            // 采用同步方式去获取一个缓存和上面的手动方式是一个原理。在build Cache的时候会提供一个createExpensiveGraph函数。
            // 查询并在缺失的情况下使用同步的方式来构建一个缓存
            Object graph = loadingCache.get(key);
    
            // 获取组key的值返回一个Map
            List<String> keys = new ArrayList<>();
            keys.add(key);
            Map<String, Object> graphs = loadingCache.getAll(keys);
            return graph;
        }
    
        @RequestMapping("/testAsyncLoading")
        public Object testAsyncLoading(Person person) {
            String key = "name1";
    
            // 查询并在缺失的情况下使用异步的方式来构建缓存
            CompletableFuture<Object> graph = asyncLoadingCache.get(key);
            // 查询一组缓存并在缺失的情况下使用异步的方式来构建缓存
            List<String> keys = new ArrayList<>();
            keys.add(key);
            CompletableFuture<Map<String, Object>> graphs = asyncLoadingCache.getAll(keys);
    
            // 异步转同步
            loadingCache = asyncLoadingCache.synchronous();
            return graph;
        }
    
        @RequestMapping("/testSizeBased")
        public Object testSizeBased(Person person) {
            LoadingCache<String, Object> cache = Caffeine.newBuilder()
                    .maximumSize(1)
                    .build(k -> createExpensiveGraph(k));
    
            cache.get("A");
            System.out.println(cache.estimatedSize());
            cache.get("B");
            // 因为执行回收的方法是异步的,所以需要调用该方法,手动触发一次回收操作。
            cache.cleanUp();
            System.out.println(cache.estimatedSize());
    
            return "";
        }
    
        @RequestMapping("/testTimeBased")
        public Object testTimeBased(Person person) {
            String key = "name1";
            // 用户测试,一个时间源,返回一个时间值,表示从某个固定但任意时间点开始经过的纳秒数。
            FakeTicker ticker = new FakeTicker();
    
            // 基于固定的到期策略进行退出
            // expireAfterAccess
            LoadingCache<String, Object> cache1 = Caffeine.newBuilder()
                    .ticker(ticker::read)
                    .expireAfterAccess(5, TimeUnit.SECONDS)
                    .build(k -> createExpensiveGraph(k));
    
            System.out.println("expireAfterAccess:第一次获取缓存");
            cache1.get(key);
    
            System.out.println("expireAfterAccess:等待4.9S后,第二次次获取缓存");
            // 直接指定时钟
            ticker.advance(4900, TimeUnit.MILLISECONDS);
            cache1.get(key);
    
            System.out.println("expireAfterAccess:等待0.101S后,第三次次获取缓存");
            ticker.advance(101, TimeUnit.MILLISECONDS);
            cache1.get(key);
    
            // expireAfterWrite
            LoadingCache<String, Object> cache2 = Caffeine.newBuilder()
                    .ticker(ticker::read)
                    .expireAfterWrite(5, TimeUnit.SECONDS)
                    .build(k -> createExpensiveGraph(k));
    
            System.out.println("expireAfterWrite:第一次获取缓存");
            cache2.get(key);
    
            System.out.println("expireAfterWrite:等待4.9S后,第二次次获取缓存");
            ticker.advance(4900, TimeUnit.MILLISECONDS);
            cache2.get(key);
    
            System.out.println("expireAfterWrite:等待0.101S后,第三次次获取缓存");
            ticker.advance(101, TimeUnit.MILLISECONDS);
            cache2.get(key);
    
            // Evict based on a varying expiration policy
            // 基于不同的到期策略进行退出
            LoadingCache<String, Object> cache3 = Caffeine.newBuilder()
                    .ticker(ticker::read)
                    .expireAfter(new Expiry<String, Object>() {
    
                        @Override
                        public long expireAfterCreate(String key, Object value, long currentTime) {
                            // Use wall clock time, rather than nanotime, if from an external resource
                            return TimeUnit.SECONDS.toNanos(5);
                        }
    
                        @Override
                        public long expireAfterUpdate(String key, Object graph,
                                                      long currentTime, long currentDuration) {
    
                            System.out.println("调用了 expireAfterUpdate:" + TimeUnit.NANOSECONDS.toMillis(currentDuration));
                            return currentDuration;
                        }
    
                        @Override
                        public long expireAfterRead(String key, Object graph,
                                                    long currentTime, long currentDuration) {
    
                            System.out.println("调用了 expireAfterRead:" + TimeUnit.NANOSECONDS.toMillis(currentDuration));
                            return currentDuration;
                        }
                    })
                    .build(k -> createExpensiveGraph(k));
    
            System.out.println("expireAfter:第一次获取缓存");
            cache3.get(key);
    
            System.out.println("expireAfter:等待4.9S后,第二次次获取缓存");
            ticker.advance(4900, TimeUnit.MILLISECONDS);
            cache3.get(key);
    
            System.out.println("expireAfter:等待0.101S后,第三次次获取缓存");
            ticker.advance(101, TimeUnit.MILLISECONDS);
            Object object = cache3.get(key);
    
            return object;
        }
    
        @RequestMapping("/testRemoval")
        public Object testRemoval(Person person) {
            String key = "name1";
            // 用户测试,一个时间源,返回一个时间值,表示从某个固定但任意时间点开始经过的纳秒数。
            FakeTicker ticker = new FakeTicker();
    
            // 基于固定的到期策略进行退出
            // expireAfterAccess
            LoadingCache<String, Object> cache = Caffeine.newBuilder()
                    .removalListener((String k, Object graph, RemovalCause cause) ->
                            System.out.printf("Key %s was removed (%s)%n", k, cause))
                    .ticker(ticker::read)
                    .expireAfterAccess(5, TimeUnit.SECONDS)
                    .build(k -> createExpensiveGraph(k));
    
            System.out.println("第一次获取缓存");
            Object object = cache.get(key);
    
            System.out.println("等待6S后,第二次次获取缓存");
            // 直接指定时钟
            ticker.advance(6000, TimeUnit.MILLISECONDS);
            cache.get(key);
    
            System.out.println("手动删除缓存");
            cache.invalidate(key);
    
            return object;
        }
    
        @RequestMapping("/testRefresh")
        public Object testRefresh(Person person) {
            String key = "name1";
            // 用户测试,一个时间源,返回一个时间值,表示从某个固定但任意时间点开始经过的纳秒数。
            FakeTicker ticker = new FakeTicker();
    
            // 基于固定的到期策略进行退出
            // expireAfterAccess
            LoadingCache<String, Object> cache = Caffeine.newBuilder()
                    .removalListener((String k, Object graph, RemovalCause cause) ->
                            System.out.printf("执行移除监听器- Key %s was removed (%s)%n", k, cause))
                    .ticker(ticker::read)
                    .expireAfterWrite(5, TimeUnit.SECONDS)
                    // 指定在创建缓存或者最近一次更新缓存后经过固定的时间间隔,刷新缓存
                    .refreshAfterWrite(4, TimeUnit.SECONDS)
                    .build(k -> createExpensiveGraph(k));
    
            System.out.println("第一次获取缓存");
            Object object = cache.get(key);
    
            System.out.println("等待4.1S后,第二次次获取缓存");
            // 直接指定时钟
            ticker.advance(4100, TimeUnit.MILLISECONDS);
            cache.get(key);
    
            System.out.println("等待5.1S后,第三次次获取缓存");
            // 直接指定时钟
            ticker.advance(5100, TimeUnit.MILLISECONDS);
            cache.get(key);
    
            return object;
        }
    
        @RequestMapping("/testWriter")
        public Object testWriter(Person person) {
            String key = "name1";
            // 用户测试,一个时间源,返回一个时间值,表示从某个固定但任意时间点开始经过的纳秒数。
            FakeTicker ticker = new FakeTicker();
    
            // 基于固定的到期策略进行退出
            // expireAfterAccess
            LoadingCache<String, Object> cache = Caffeine.newBuilder()
                    .removalListener((String k, Object graph, RemovalCause cause) ->
                            System.out.printf("执行移除监听器- Key %s was removed (%s)%n", k, cause))
                    .ticker(ticker::read)
                    .expireAfterWrite(5, TimeUnit.SECONDS)
                    .writer(new CacheWriter<String, Object>() {
                        @Override
                        public void write(String key, Object graph) {
                            // write to storage or secondary cache
                            // 写入存储或者二级缓存
                            System.out.printf("testWriter:write - Key %s was write (%s)%n", key, graph);
                            createExpensiveGraph(key);
                        }
    
                        @Override
                        public void delete(String key, Object graph, RemovalCause cause) {
                            // delete from storage or secondary cache
                            // 删除存储或者二级缓存
                            System.out.printf("testWriter:delete - Key %s was delete (%s)%n", key, graph);
                        }
                    })
                    // 指定在创建缓存或者最近一次更新缓存后经过固定的时间间隔,刷新缓存
                    .refreshAfterWrite(4, TimeUnit.SECONDS)
                    .build(k -> createExpensiveGraph(k));
    
            cache.put(key, personService.findOne1());
            cache.invalidate(key);
    
            System.out.println("第一次获取缓存");
            Object object = cache.get(key);
    
            System.out.println("等待4.1S后,第二次次获取缓存");
            // 直接指定时钟
            ticker.advance(4100, TimeUnit.MILLISECONDS);
            cache.get(key);
    
            System.out.println("等待5.1S后,第三次次获取缓存");
            // 直接指定时钟
            ticker.advance(5100, TimeUnit.MILLISECONDS);
            cache.get(key);
    
            return object;
        }
    
        @RequestMapping("/testStatistics")
        public Object testStatistics(Person person) {
            String key = "name1";
            // 用户测试,一个时间源,返回一个时间值,表示从某个固定但任意时间点开始经过的纳秒数。
            FakeTicker ticker = new FakeTicker();
    
            // 基于固定的到期策略进行退出
            // expireAfterAccess
            LoadingCache<String, Object> cache = Caffeine.newBuilder()
                    .removalListener((String k, Object graph, RemovalCause cause) ->
                            System.out.printf("执行移除监听器- Key %s was removed (%s)%n", k, cause))
                    .ticker(ticker::read)
                    .expireAfterWrite(5, TimeUnit.SECONDS)
                    // 开启统计
                    .recordStats()
                    // 指定在创建缓存或者最近一次更新缓存后经过固定的时间间隔,刷新缓存
                    .refreshAfterWrite(4, TimeUnit.SECONDS)
                    .build(k -> createExpensiveGraph(k));
    
            for (int i = 0; i < 10; i++) {
                cache.get(key);
                cache.get(key + i);
            }
            // 驱逐是异步操作,所以这里要手动触发一次回收操作
            ticker.advance(5100, TimeUnit.MILLISECONDS);
            // 手动触发一次回收操作
            cache.cleanUp();
    
            System.out.println("缓存命数量:" + cache.stats().hitCount());
            System.out.println("缓存命中率:" + cache.stats().hitRate());
            System.out.println("缓存逐出的数量:" + cache.stats().evictionCount());
            System.out.println("加载新值所花费的平均时间:" + cache.stats().averageLoadPenalty());
    
            return cache.get(key);
        }
    
        @RequestMapping("/testPolicy")
        public Object testPolicy(Person person) {
            FakeTicker ticker = new FakeTicker();
    
            LoadingCache<String, Object> cache = Caffeine.newBuilder()
                    .ticker(ticker::read)
                    .expireAfterAccess(5, TimeUnit.SECONDS)
                    .maximumSize(1)
                    .build(k -> createExpensiveGraph(k));
    
            // 在代码里面动态的指定最大Size
            cache.policy().eviction().ifPresent(eviction -> {
                eviction.setMaximum(4 * eviction.getMaximum());
            });
    
            cache.get("E");
            cache.get("B");
            cache.get("C");
            cache.cleanUp();
            System.out.println(cache.estimatedSize() + ":" + JSON.toJSON(cache.asMap()).toString());
    
            cache.get("A");
            ticker.advance(100, TimeUnit.MILLISECONDS);
            cache.get("D");
            ticker.advance(100, TimeUnit.MILLISECONDS);
            cache.get("A");
            ticker.advance(100, TimeUnit.MILLISECONDS);
            cache.get("B");
            ticker.advance(100, TimeUnit.MILLISECONDS);
            cache.policy().eviction().ifPresent(eviction -> {
                // 获取热点数据Map
                Map<String, Object> hottestMap = eviction.hottest(10);
                // 获取冷数据Map
                Map<String, Object> coldestMap = eviction.coldest(10);
    
                System.out.println("热点数据:" + JSON.toJSON(hottestMap).toString());
                System.out.println("冷数据:" + JSON.toJSON(coldestMap).toString());
            });
    
            ticker.advance(3000, TimeUnit.MILLISECONDS);
            // ageOf通过这个方法来查看key的空闲时间
            cache.policy().expireAfterAccess().ifPresent(expiration -> {
    
                System.out.println(JSON.toJSON(expiration.ageOf("A", TimeUnit.MILLISECONDS)));
            });
            return cache.get("name1");
        }
    }
    

    英文不好,有些翻译的不准确的请不吝赐教。。。

    参考:

    源码:
    https://github.com/wyh-spring-ecosystem-student/spring-boot-student/tree/releases

    spring-boot-student-cache-caffeine 工程

    为监控而生的多级缓存框架 layering-cache这是我开源的一个多级缓存框架的实现,如果有兴趣可以看一下。

    展开全文
  • Caffeine is a high performance, near optimal caching library based on Java 8. For more details, see our user's guide and browse the API docs for the latest release. Cache Caffeine provides an in-...
  • caffeine java_Caffeine用法

    2021-02-26 10:50:34
    Caffeine是使用Java8对Guava缓存的重写版本,在Spring Boot 2.0中将取代Guava。如果出现Caffeine,CaffeineCacheManager将会自动配置。使用spring.cache.cache-names属性可以在启动时创建缓存,并可以通过以下配置...

    Caffeine是使用Java8对Guava缓存的重写版本,在Spring Boot 2.0中将取代Guava。如果出现Caffeine,CaffeineCacheManager将会自动配置。使用spring.cache.cache-names属性可以在启动时创建缓存,并可以通过以下配置进行自定义(按顺序):

    spring.cache.caffeine.spec: 定义的特殊缓存

    com.github.benmanes.caffeine.cache.CaffeineSpec: bean定义

    com.github.benmanes.caffeine.cache.Caffeine: bean定义

    例如,以下配置创建一个foo和bar缓存,最大数量为500,存活时间为10分钟:

    spring.cache.cache-names=foo,bar

    spring.cache.caffeine.spec=maximumSize=500,expireAfterAccess=600s

    除此之外,如果定义了com.github.benmanes.caffeine.cache.CacheLoader,它会自动关联到CaffeineCacheManager。由于该CacheLoader将关联被该缓存管理器管理的所有缓存,所以它必须定义为CacheLoader,自动配置将忽略所有泛型类型。

    1.引入依赖

    2.7.0

    org.springframework.boot

    spring-boot-starter-cache

    com.github.ben-manes.caffeine

    caffeine

    ${caffeine.cache.version}

    2.configuration

    1 @EnableCaching2 @Configuration3 public classCaffeineCacheConfig {4 public static final Integer CAFFEINE_MAXSIZE = PropertyUtil.getInt("caffeine.maxsize", "10000");5 public static final Integer CAFFEINE_EXPIRE_TIME = PropertyUtil.getInt("caffeine.expire.time", "3");6

    7 /**

    8 * 创建基于Caffeine的Cache Manager9 *@return

    10 */

    11 @Bean("caffeineCacheManager")12 publicCacheManager CaffeineCacheManager() {13 CaffeineCacheManager cacheManager = newCaffeineCacheManager();14

    15 cacheManager.setCaffeine(Caffeine.newBuilder().recordStats()16 .expireAfterWrite(CAFFEINE_EXPIRE_TIME, TimeUnit.SECONDS)17 .maximumSize(CAFFEINE_MAXSIZE));18

    19 returncacheManager;20 }21

    22 }

    使用@EnableCaching注解让Spring Boot开启对缓存的支持

    Caffeine配置说明:

    initialCapacity=[integer]: 初始的缓存空间大小

    maximumSize=[long]: 缓存的最大条数

    maximumWeight=[long]: 缓存的最大权重

    expireAfterAccess=[duration]: 最后一次写入或访问后经过固定时间过期

    expireAfterWrite=[duration]: 最后一次写入后经过固定时间过期

    refreshAfterWrite=[duration]: 创建缓存或者最近一次更新缓存后经过固定的时间间隔,刷新缓存

    weakKeys: 打开key的弱引用

    weakValues:打开value的弱引用

    softValues:打开value的软引用

    recordStats:开发统计功能

    注意:

    expireAfterWrite和expireAfterAccess同事存在时,以expireAfterWrite为准。

    maximumSize和maximumWeight不可以同时使用

    weakValues和softValues不可以同时使用

    3.service

    1 @Service("caffeineCacheService")2 public classCaffeineCacheServiceImpl {3 @Autowired4 CacheManager caffeineCacheManager;5

    6 private final static String DEFAULT_CACHE = "default";7

    8 public T getValue(Object key) {9 if(key == null) return null;10

    11 Cache cache =caffeineCacheManager.getCache(DEFAULT_CACHE);12 if(cache != null) {13 Cache.ValueWrapper wrapper =cache.get(key);14 if (wrapper != null)15 return(T) wrapper.get();16 }17

    18 return null;19 }20

    21 public T getValue(String cacheName, Object key) {22 if(cacheName == null || key == null) return null;23

    24 Cache cache =caffeineCacheManager.getCache(cacheName);25 if(cache != null) {26 Cache.ValueWrapper wrapper =cache.get(key);27 if (wrapper != null)28 return(T) wrapper.get();29 }30

    31 return null;32 }33

    34 public voidputValue(Object key, Object value) {35 if(key == null || value == null) return;36

    37 Cache cache =caffeineCacheManager.getCache(DEFAULT_CACHE);38 if(cache != null) {39 cache.put(key, value);40 }41 }42

    43 public voidputValue(String cacheName, Object key, Object value) {44 if(cacheName == null || key == null || value == null) return;45

    46 Cache cache =caffeineCacheManager.getCache(cacheName);47 if(cache != null) {48 cache.put(key, value);49 }50 }51 }

    其中get(key)只是返回了ValueWrapper,具体value需要get方法。我看了caffeineCacheManager.getCache方法,按理说在cachemap中找不到cache的时候会新建一个cache并放入map中再返回,但是看了源码方法上标注了@Nullable,为了代码严谨,选择了判断null。

    4.实例

    1 private static Integer uuid = 0;2 @Cacheable(value = DEFAULT_CACHE, key = "#pin")3 publicInteger getUUid(String pin) {4 /*

    5 if(getValue(pin) != null) {6 return getValue(pin);7 }*/

    8

    9 return uuid++;10 }

    附:spring cache相关注解介绍 @Cacheable、@CachePut、@CacheEvict

    @Cacheable

    @Cacheable是用来声明方法是可缓存的。将结果存储到缓存中以便后续使用相同参数调用时不需执行实际的方法。直接从缓存中取值。最简单的格式需要制定缓存名称。

    例如:

    1 @Cacheable("books")2 public Book findBook(ISBN isbn) {...}

    在上面的代码片段中,findBook方法与名为books的缓存想关联。每次调用该方法时,将在缓存中检查该请求是否已执行,以免重复执行。虽然在大多数情况下,只有一个缓存被声明,注释允许指定多个名称,以便使用多个缓存。这种情况下,在执行方法之前,每个缓存都会检查之前执行的方法,只要有一个缓存命中,即直接从缓存中返回相关的值。

    即使没有实际执行缓存方法,所有其他不包含该值的缓存也将被更新。

    例如:

    1 @Cacheable({"books", "isbns"})2 public Book findBook(ISBN isbn) {...}

    默认key生成:

    默认key的生成按照以下规则:

    - 如果没有参数,则使用0作为key

    - 如果只有一个参数,使用该参数作为key

    - 如果又多个参数,使用包含所有参数的hashCode作为key

    自定义key的生成:

    当目标方法参数有多个时,有些参数并不适合缓存逻辑

    比如:

    1 @Cacheable("books")2 public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

    其中checkWarehouse,includeUsed并不适合当做缓存的key.针对这种情况,Cacheable 允许指定生成key的关键属性,并且支持支持SpringEL表达式。(推荐方法)

    再看一些例子:

    1 @Cacheable(cacheNames="books", key="#isbn")2 public Book findBook(ISBN isbn, boolean checkWarehouse, booleanincludeUsed)3

    4 @Cacheable(cacheNames="books", key="#isbn.rawNumber")5 public Book findBook(ISBN isbn, boolean checkWarehouse, booleanincludeUsed)6

    7 @Cacheable(cacheNames="books", key="T(someType).hash(#isbn)")8 public Book findBook(ISBN isbn, boolean checkWarehouse, booleanincludeUsed)9

    10 @Cacheable(cacheNames="books", key="#map['bookid'].toString()")11 public Book findBook(Map map)

    缓存的同步 sync:

    在多线程环境下,某些操作可能使用相同参数同步调用。默认情况下,缓存不锁定任何资源,可能导致多次计算,而违反了缓存的目的。对于这些特定的情况,属性 sync 可以指示底层将缓存锁住,使只有一个线程可以进入计算,而其他线程堵塞,直到返回结果更新到缓存中。

    例:

    1 @Cacheable(cacheNames="foos", sync="true")2 public Foo executeExpensiveOperation(String id) {...}

    属性condition:

    有时候,一个方法可能不适合一直缓存(例如:可能依赖于给定的参数)。属性condition支持这种功能,通过SpEL 表达式来指定可求值的boolean值,为true才会缓存(在方法执行之前进行评估)。

    例:

    1 @Cacheable(cacheNames="book", condition="#name.length < 32")2 public Book findBook(String name)

    此外,还有一个unless 属性可以用来是决定是否添加到缓存。与condition不同的是,unless表达式是在方法调用之后进行评估的。如果返回false,才放入缓存(与condition相反)。 #result指返回值 例:

    1 @Cacheable(cacheNames="book", condition="#name.length < 32", unless="#result.name.length > 5"")

    2 public Book findBook(String name)

    @CachePut

    如果缓存需要更新,且不干扰方法的执行,可以使用注解@CachePut。@CachePut标注的方法在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中。

    1 @CachePut(cacheNames="book", key="#isbn")2 public Book updateBook(ISBN isbn, BookDescriptor descriptor)

    注意:应该避免@CachePut 和 @Cacheable同时使用的情况。

    @CacheEvict

    spring cache不仅支持将数据缓存,还支持将缓存数据删除。此过程经常用于从缓存中清除过期或未使用的数据。

    @CacheEvict要求指定一个或多个缓存,使之都受影响。此外,还提供了一个额外的参数allEntries 。表示是否需要清除缓存中的所有元素。默认为false,表示不需要。当指定了allEntries为true时,Spring Cache将忽略指定的key。有的时候我们需要Cache一下清除所有的元素。

    1 @CacheEvict(cacheNames="books", allEntries=true)2 public void loadBooks(InputStream batch)

    清除操作默认是在对应方法成功执行之后触发的,即方法如果因为抛出异常而未能成功返回时也不会触发清除操作。使用beforeInvocation可以改变触发清除操作的时间,当我们指定该属性值为true时,Spring会在调用该方法之前清除缓存中的指定元素。

    1 @CacheEvict(cacheNames="books", beforeInvocation=true)2 public void loadBooks(InputStream batch)

    @CacheConfig

    有时候一个类中可能会有多个缓存操作,而这些缓存操作可能是重复的。这个时候可以使用@CacheConfig

    1 @CacheConfig("books")2 public class BookRepositoryImpl implementsBookRepository {3

    4 @Cacheable5 publicBook findBook(ISBN isbn) {...}6 }

    @CacheConfig是一个类级别的注解,允许共享缓存的名称、KeyGenerator、CacheManager 和CacheResolver。

    该操作会被覆盖。

    开启缓存注解

    java类配置:

    1 @Configuration2 @EnableCaching3 public classAppConfig {4 }

    XML 配置:

    http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd

    http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd">

    展开全文
  • 学习Caffeine

    2020-11-25 18:31:40
    Caffeine背景 Caffeine 是基于 JAVA 8 的高性能缓存库。并且在 spring5 (springboot 2.x) 后,spring 官方放弃了 Guava,而使用了性能更优秀的 Caffeine 作为默认缓存组件。 Caffeine wiki Caffeine配置参数 图片...

    Caffeine背景

    Caffeine 是基于 JAVA 8 的高性能缓存库。并且在 spring5 (springboot 2.x) 后,spring 官方放弃了 Guava,而使用了性能更优秀的 Caffeine 作为默认缓存组件。

    Caffeine wiki

    Caffeine配置参数

    图片来源
    在这里插入图片描述
    Caffeine配置参数时可以直接调用对应的方法进行设置,同时也提供了一个简单的字符格式配置,由CaffeineSpec负责解析。这里的字符串语法是一系列由逗号隔开的键值对组成,其中每个键值对对应一个配置方法。但是这里的字符配置不支持需要对象来作为参数的配置方法。

     // 这个运行时会报错,maximumSize与maximumWeight不能一起使用
     Cache<Object, Object> cache = Caffeine.from ("maximumSize=10,maximumWeight=10").build ();
    

    持续时间可以通过在一个integer类型之后跟上一个"d",“h”,“m”,或者"s"来分别表示天,小时,分钟或者秒。另外,从2.8.7版本开始,ISO-8601标准的字符串也将被支持来配置持续时间,并通过Duration.parse来进行解析。出于表示缓存持续时间的目的,这里不支持配置负的持续时间,并将会抛出异常。两种持续时间表示格式的示例如下所示。
    在这里插入图片描述

    注意:maximumSize 和 maximumWeight 不可以同时使用。weakValues 和 softValues 不可以同时使用。

    Caffeine简单使用

    缓存添加

    Caffeine添加缓存的策略分为4种:手动加载,自动加载,手动异步加载和自动异步加载。构建缓存分为两种,同步缓存Cache和异步缓存AsyncCache。它们之间有几个方法,方法签名相同,作用类似,只是返回值不同,这里单独列出来说明一下。

    V getIfPresent(@NonNull @CompatibleWith("K") Object key);
    --------
    CompletableFuture<V> getIfPresent(@NonNull @CompatibleWith("K") Object key);
    方法释义:它们会根据缓存key查找对应缓存,缓存存在直接返回,不存在,返回null
    
    V get(@NonNull K key, @NonNull Function<? super K, ? extends V> mappingFunction);
    Map<K, V> getAll(@NonNull Iterable<? extends @NonNull K> keys,
          @NonNull Function<Iterable<? extends @NonNull K>, @NonNull Map<K, V>> mappingFunction);
    --------
    CompletableFuture<V> get(@NonNull K key,
          @NonNull Function<? super K, ? extends V> mappingFunction);
    CompletableFuture<Map<K, V>> getAll(@NonNull Iterable<? extends @NonNull K> keys,
          @NonNull Function<Iterable<? extends @NonNull K>, @NonNull Map<K, V>> mappingFunction);
    方法释义:这两个方法分别为单个查找和批量查找。根据缓存key查找对应缓存,缓存存在直接返回,不存在,则根据function创建,缓存后返回。
    
    ConcurrentMap<@NonNull K, @NonNull V> asMap();
    --------
    ConcurrentMap<@NonNull K, @NonNull CompletableFuture<V>> asMap();
    方法释义:通过这个方法可以看出来Caffeine底层缓存是通过ConcurrentMap实现的,这里直接暴露出来给我们使用,
    需要小心,因为对它的任何修改将直接影响到缓存。根据ConcurrentMap获取的迭代器是弱一致性的,并不能保证其他线程的修改会在迭代器中展示。
    
    		<!--我使用的springBoot版本为2.3.4.RELEASE -->
            <dependency>
                <groupId>com.github.ben-manes.caffeine</groupId>
                <artifactId>caffeine</artifactId>
            </dependency>
    
    • 手动加载
    	@Test
        public void test(){
            Cache<Object, Object> cache = Caffeine.newBuilder ()
                    .expireAfterWrite (10, TimeUnit.SECONDS)
                    .maximumSize (10)
                    .build ();
            // 添加或者更新一个缓存元素
            cache.put("hello", "world");
            // 查找一个缓存元素, 没有查找到的时候返回null
            System.out.println (cache.getIfPresent("hello"));
            // 移除一个缓存元素
            cache.invalidate("hello");
            Object hello = cache.get ("hello", k -> String.format ("没有找到对应的键值%s", k));
            System.out.println (hello);
        }
    
    • 自动加载
        @Test
        public void test4(){
            LoadingCache<Object, Object> cache = Caffeine.newBuilder ()
                    .expireAfterWrite (5, TimeUnit.SECONDS)
                    .maximumSize (20)
                    .build (new CacheLoader<Object, Object> () {
                        // 如果key对应的缓存不存在,将调用load方法来生成缓存元素
                        @Override
                        public @Nullable Object load(@NonNull Object o) throws Exception {
                            System.out.println ("加载缓存中" + o.toString ());
                            return o.toString () + System.currentTimeMillis ();
                        }
                        // 如果没有重写 loadAll 方法则默认的 loadAll 回循环调用 load 方法
                        // 在批量检索比单个查找更有效率的场景下,你可以覆盖并开发CacheLoader.loadAll 方法来使你的缓存更有效率。
                        /*@Override
                        public @NonNull Map<Object, Object> loadAll(@NonNull Iterable<?> keys) throws Exception {
                            return null;
                        }*/
                    });
            cache.get ("hello1");
            System.out.println (cache.get ("hello1"));
            cache.get ("hello2");
            cache.get ("hello3");
            cache.getAll (Arrays.asList ("hello4","hello5","hello6"));
        }
    // 测试结果
    加载缓存中hello1
    hello11606209690594
    加载缓存中hello2
    加载缓存中hello3
    加载缓存中hello4
    加载缓存中hello5
    加载缓存中hello6
    
    • 手动异步加载
    @Test
        public void test5() throws InterruptedException {
            ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor (2, 5, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable> ());
            AsyncCache<Object, Object> asyncCache = Caffeine.newBuilder ()
                    .expireAfterWrite (10, TimeUnit.SECONDS)
                    .maximumSize (10)
                    // 默认的线程池实现是 ForkJoinPool.commonPool()
                    .executor (poolExecutor)
                    .buildAsync ();
            // 
            CompletableFuture<Object> future = asyncCache.get ("hello", o -> {
                try {
                    Thread.sleep (5000);
                } catch (InterruptedException e) {
                    e.printStackTrace ();
                }
                return o.toString () + System.currentTimeMillis ();
            });
            // 将在启动后第6秒打印 hello1606212949380
            future.thenAccept (System.out::println);
            Thread.sleep (6000);
        }
    
    • 自动异步加载:也就是在手动异步加载的基础上使用CacheLoader,实际上源码将CacheLoader向上造型为AsyncCacheLoader。

    Caffeine使缓存失效可以通过手动移除(即调用invalidate相关方法),也可以通过策略移除,策略分为3种。

    驱逐策略 对应方法
    基于容量 maximumSize / maximumWeight
    基于引用 weakKeys / weakValues / softValues
    基于时间 expireAfterAccess /expireAfterWrite / expireAfter

    基于容量即达到我们设定的值的大小后,将采取相关算法移除缓存,权重的话我们需要提供缓存元素的权重值,通过weigher设定;

    基于引用,首先需要明白什么是弱引用,什么是软引用,只被软引用关联对象的生命周期是当内存空间不足,执行垃圾回收时,才会进行回收的一种引用,而只被弱引用关联对象只能活到下一次垃圾回收。使用这种策略时,是让垃圾回收器帮忙移除缓存。另外,官方文档种指出使用软引用比较影响性能,建议我们使用基于缓存容量的策略。还有一点,在判断缓存key是否相同时,将不再基于equals方法,而是==

    基于时间也很好理解,无非是指定多长时间,或者读写多长时间后。

    缓存驱逐

    以上3种驱逐策略当满足指定条件后并不会马上移除缓存,我们可以通过removalListener定义一个移除监听器在一个元素被移除的时候进行相应的操作,通过测试我发现满足指定驱逐条件后,当再次访问缓存时,会进行移除。如果想要立即移除的话,可以通过调用invalidate相关方法。

    	@Test
        public void test6() {
            FakeTicker ticker = new FakeTicker ();
            Cache<Object, Object> cache = Caffeine.newBuilder ()
                    .expireAfterAccess (5, TimeUnit.SECONDS)
                    .maximumSize (2).ticker (ticker).removalListener(new RemovalListener<Object, Object>() {
                        @Override
                        public void onRemoval(@Nullable Object k, @Nullable Object v, @NonNull RemovalCause removalCause) {
                            System.out.println("缓存失效了 removed " + k + " cause " + removalCause.toString());
                        }
                    })
                    .build ();
            // 添加或者更新一个缓存元素
            cache.put("hello", "world");
            // 通过这个可以避免等待5秒才触发驱逐策略
            ticker.advance (5, TimeUnit.SECONDS);
            //System.out.println (cache.get ("hello", k -> String.format ("没有找到对应的键值%s", k)));
        }
        class FakeTicker implements Ticker {
            private final AtomicLong nanos = new AtomicLong();
    
            public FakeTicker advance(long time, TimeUnit timeUnit) {
                nanos.addAndGet(timeUnit.toNanos(time));
                return this;
            }
    
            @Override
            public long read() {
                return this.nanos.get();
            }
        }
    
    缓存更新

    缓存更新策略提供了一个方法refreshAfterWrite,在写操作之后的一段时间后允许key对应的缓存元素进行刷新,但是只有在这个key被真正查询到的时候才会正式进行刷新操作。记住,refreshAfterWrite需要配合LoadingCache或AsyncLoadingCache使用,否则会报错。

    	@Test
        public void test8() {
        	// 这个类在上面的demo里面有
            FakeTicker ticker = new FakeTicker ();
            LoadingCache<Object, Object> cache = Caffeine.newBuilder ()
                    .refreshAfterWrite (5, TimeUnit.SECONDS)
                    .maximumSize (2).ticker (ticker)
                    .build (new CacheLoader<Object, Object> () {
                        // 如果key对应的缓存不存在,将调用load方法来生成缓存元素
                        @Override
                        public @Nullable Object load(@NonNull Object o) throws Exception {
                            System.out.println ("加载缓存中" + o.toString ());
                            return o.toString () + System.currentTimeMillis ();
                        }
                    });
            // 添加或者更新一个缓存元素
            cache.put("hello", "world");
            System.out.println (cache.getIfPresent ("hello"));
            // 这里必须设置成大于5s,才允许对缓存进行刷新操作
            ticker.advance (6, TimeUnit.SECONDS);
            // 这里查询返回的值依然是world,但是打印出加载缓存中
            System.out.println (cache.getIfPresent ("hello"));
            // 这里再次查询返回新值
            System.out.println (cache.getIfPresent ("hello"));
            // 显式调用这个方法后,再次查询会马上返回新值
            //cache.refresh ("hello");
            //System.out.println (cache.getIfPresent ("hello"));
        }
    

    动态调整策略

    基于容量

    	@Test
        public void test12() {
            Cache<Object, Object> cache = Caffeine.newBuilder ()
                    .maximumSize (2)
                    .build ();
            cache.policy ().eviction ().ifPresent (eviction ->{
            	// 调整缓存最大条数为原来的两倍
                eviction.setMaximum (2 * eviction.getMaximum());
            });
            cache.put("hello", "world");
            cache.put("hello1", "world");
            cache.put("hello2", "world");
            cache.put("hello3", "world");
            System.out.println (cache.getIfPresent ("hello2"));
            System.out.println (cache.getIfPresent ("hello3"));
        }
        输出: world 
        	   world
    ``
    基于时间
    
    ```java
     	@Test
        public void test11() {
            FakeTicker ticker = new FakeTicker ();
            Cache<Object, Object> cache = Caffeine.newBuilder ()
                    .maximumSize (2)
                    .expireAfterAccess (5, TimeUnit.SECONDS)
                    .ticker (ticker)
                    .build ();
            cache.put("hello", "world");
            cache.policy ().expireAfterAccess ().ifPresent (expiration -> {
                // ageOf(key, TimeUnit)方法提供了查看缓存元素在expireAfterAccess,expireAfterWrite或者 refreshAfterWrite 策略下的空闲时间的途径
                System.out.println (expiration.ageOf ("hello",TimeUnit.SECONDS).getAsLong ());
                // 缓存中的元素最大可持续时间可以通过getExpiresAfter(TimeUnit)方法获取
                System.out.println (expiration.getExpiresAfter (TimeUnit.SECONDS));
                // 可以动态调整时间
                expiration.setExpiresAfter (2, TimeUnit.SECONDS);
            });
            ticker.advance (2, TimeUnit.SECONDS);
            // 此时缓存失效
            System.out.println (cache.getIfPresent ("hello"));
        }
    

    统计指标

    可以通过调用Cache#stats返回一个指标对象CacheStats。在CacheStats里有很多统计好的属性可以查阅,这里就不展示了。

    了解Caffeine的实现原理

    Caffine Cache 在算法上的优点

    springBoot中使用Caffeine作为缓存

    yaml配置

    spring:
    	cache:
          type: caffeine
          cache-names: teacher
          caffeine:
            spec: maximumSize=500,expireAfterAccess=600s
    

    Java配置

    @EnableCaching
    @Configuration
    public class CacheConfig {
        /**
         * 配置缓存管理器
         */
        @Bean
        public CacheManager cacheManager() {
            CaffeineCacheManager cacheManager = new CaffeineCacheManager();
            cacheManager.setCaffeine(Caffeine.newBuilder()
                    // 设置最后一次写入或访问后经过固定时间过期
                    .expireAfterAccess(60, TimeUnit.SECONDS)
                    // 初始的缓存空间大小
                    .initialCapacity(100)
                    // 缓存的最大条数
                    .maximumSize(1000));
            return cacheManager;
        }
    
    }
    

    至于缓存使用我在另一篇博客里面有终结

    展开全文
  • Caffeine入门

    2020-12-08 22:09:50
    Caffeine使用指南 Caffeine基于java8的高性能,接近最优的缓存库。Caffeine提供的内存缓存使用参考Google guava的API。Caffeine是基于Google guava和 ConcurrentLinkedHashMap的设计经验上改进的成果。 Caffeine...

     

    Caffeine使用指南

    Caffeine基于java8的高性能,接近最优的缓存库。Caffeine提供的内存缓存使用参考Google guava的API。Caffeine是基于Google guava和 ConcurrentLinkedHashMap的设计经验上改进的成果。

    Caffeine可以通过建造者模式灵活的组合以下特性:

    1. 通过异步自动加载实体到缓存中
    2. 基于大小的回收策略
    3. 基于时间的回收策略
    4. 自动刷新
    5. key自动封装虚引用
    6. value自动封装弱引用或软引用
    7. 实体过期或被删除的通知
    8. 写入外部资源
    9. 统计累计访问缓存

    加载策略

    Caffeine提供了3种加载策略:手动加载,同步加载,异步加载

    手动加载

    Cache<Key, Graph> cache = Caffeine.newBuilder()
        .expireAfterWrite(10, TimeUnit.MINUTES)
        .maximumSize(10_000)
        .build();
    // 检索一个entry,如果没有则为null
    Graph graph = cache.getIfPresent(key);
    // 检索一个entry,如果entry为null,则通过key创建一个entry并加入缓存
    graph = cache.get(key, k -> createExpensiveGraph(key));
    // 插入或更新一个实体
    cache.put(key, graph);
    // 移除一个实体
    cache.invalidate(key);
    

    同步加载

    构造Cache时候,build方法传入一个CacheLoader实现类。实现load方法,通过key加载value。

    LoadingCache<Key, Graph> cache = Caffeine.newBuilder()
        .maximumSize(10_000)
        .expireAfterWrite(10, TimeUnit.MINUTES)
        .build(key -> createExpensiveGraph(key));
    //如果缓存种没有对应的value,通过createExpensiveGraph方法加载
    Graph graph = cache.get(key);
    
    Map<Key, Graph> graphs = cache.getAll(keys);
    

    异步加载

    AsyncLoadingCache<Key, Graph> cache = Caffeine.newBuilder()
        .maximumSize(10_000)
        .expireAfterWrite(10, TimeUnit.MINUTES)
        .buildAsync((key, executor) -> createExpensiveGraphAsync(key, executor));
    CompletableFuture<Graph> graph = cache.get(key);
    CompletableFuture<Map<Key, Graph>> graphs = cache.getAll(keys);
    

    AsyncLoadingCache 是 LoadingCache 的变体, 可以异步计算实体在一个线程池(Executor)上并且返回 CompletableFuture.

    回收策略

    Caffeine提供了3种回收策略:基于大小回收,基于时间回收,基于引用回收

    基于大小回收

    // 基于实体数量淘汰实体
    LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
        .maximumSize(10_000)
        .build(key -> createExpensiveGraph(key));
    
    // 通过权重来计算,每个实体都有不同的权重,总权重到达最高时淘汰实体。
    LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
        .maximumWeight(10_000)
        .weigher((Key key, Graph graph) -> graph.vertices().size())
        .build(key -> createExpensiveGraph(key));
    

    到达最大大小时淘汰最近最少使用的实体

    基于时间回收

    1. 实体被访问之后,在实体被读或被写后的一段时间后过期
    LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
        .expireAfterAccess(5, TimeUnit.MINUTES)
        .build(key -> createExpensiveGraph(key));
    
    1. 基于写之后,在实体被写入后的一段时间后过期
    LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
        .expireAfterWrite(10, TimeUnit.MINUTES)
        .build(key -> createExpensiveGraph(key));
    
    1. 自定义策略Expiry,可以自定义在实体被读,被更新,被创建后的时间过期。
    LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
        .expireAfter(new Expiry<Key, Graph>() {
          public long expireAfterCreate(Key key, Graph graph, long currentTime) {
            // Use wall clock time, rather than nanotime, if from an external resource
            long seconds = graph.creationDate().plusHours(5)
                .minus(System.currentTimeMillis(), MILLIS)
                .toEpochSecond();
            return TimeUnit.SECONDS.toNanos(seconds);
          }
          public long expireAfterUpdate(Key key, Graph graph, 
              long currentTime, long currentDuration) {
            return currentDuration;
          }
          public long expireAfterRead(Key key, Graph graph,
              long currentTime, long currentDuration) {
            return currentDuration;
          }
        })
        .build(key -> createExpensiveGraph(key));
    

    基于引用回收

    java种有四种引用:强引用,软引用,弱引用和虚引用,caffeine可以将值封装成弱引用或软引用。
    软引用:如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。
    弱引用:弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存

    LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
        .weakKeys()
        .weakValues()
        .build(key -> createExpensiveGraph(key));
    
    
    LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
        .softValues()
        .build(key -> createExpensiveGraph(key));
    
    

    自动刷新

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

    在写后的持续时间过后,调用createExpensiveGraph刷新

    移除通知

    Cache<Key, Graph> graphs = Caffeine.newBuilder()
        .removalListener((Key key, Graph graph, RemovalCause cause) ->
            System.out.printf("Key %s was removed (%s)%n", key, cause))
        .build();
    

    通过removalListener添加实体移除监听器

    写到外部存储

    通过CacheWriter 可以将缓存回写的外部存储中。

    LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
      .writer(new CacheWriter<Key, Graph>() {
        @Override public void write(Key key, Graph graph) {
          // 写入到外部存储或二级缓存
        }
        @Override public void delete(Key key, Graph graph, RemovalCause cause) {
          // 删除外部存储或者二级缓存
        }
      })
      .build(key -> createExpensiveGraph(key));
    
    

    使用场景

    1. 缓存同步数据库
    2. 多级缓存同步

    注意,CacheWriter不能与弱键或AsyncLoadingCache一起使用

    统计缓存使用情况

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

    通过使用Caffeine.recordStats(), 可以转化成一个统计的集合. 通过 Cache.stats() 返回一个CacheStats。CacheStats提供以下统计方法

    hitRate(): 返回缓存命中率
    evictionCount(): 缓存回收数量
    averageLoadPenalty(): 加载新值的平均时间

    展开全文
  • Caffeine用法

    2020-12-11 14:23:17
    Caffeine是使用Java8对Guava缓存的重写版本,在Spring Boot 2.0中将取代Guava。如果出现Caffeine,CaffeineCacheManager将会自动配置。使用spring.cache.cache-names属性可以在启动时创建缓存,并可以通过以下配置...
  • caffeine.zip

    2019-09-24 09:26:47
    caffeine.zip,加速你的android开发
  • Caffeine缓存

    2020-11-08 12:59:29
    在本文中,我们来看看 Caffeine — 一个高性能的 Java 缓存库。 缓存和 Map 之间的一个根本区别在于缓存可以回收存储的 item。 回收策略为在指定时间删除哪些对象。此策略直接影响缓存的命中率 — 缓存库的一个重要...
  • Caffeine示例

    2020-09-14 20:49:09
    @SneakyThrows ... cache = Caffeine.newBuilder() .maximumSize(5) .expireAfterWrite(10, TimeUnit.MINUTES) .build(key -> { // 加载时,睡眠一秒 Thread.sleep(1000); return key + System.currentTi
  • Caffeine使用指南

    万次阅读 2019-07-19 10:02:07
    Caffeine使用指南 Caffeine基于java8的高性能,接近最优的缓存库。Caffeine提供的内存缓存使用参考Google guava的API。Caffeine是基于Google guava和 ConcurrentLinkedHashMap的设计经验上改进的成果。 Caffeine可以...
  • Springboot 集成 Caffeine

    千次阅读 2019-06-19 16:34:01
    Springboot 集成 Caffeine 我的博客: http://www.izuul.com Caffeine 是个高性能的开源 Java 内存缓存库,具有较高的命中率和出色的并发能力。在 Spring Boot 中集成也非常简单,提供了各种开箱既用的工具。 ...
  • Caffeine Cache

    千次阅读 2019-06-26 17:25:58
    在多线程高并发场景中往往是离不开cache的,需要根据不同的应用场景来需要选择不同的cache,比如分布式缓存如redis、memcached,还有本地(进程内)缓存如ehcache、GuavaCache、Caffeine。 说起Guava Cache,很多人...
  • https://www.jianshu.com/p/5c04d0cd3c1f 使用举例: https://www.cnblogs.com/CrankZ/p/10889859.html 引入maven工程: ... // https://mvnrepository.com/artifact/com.github.ben-manes.caffeine/caffeine compile ...
  • Caffeine Cache实现

    2020-03-13 17:14:47
    Caffeine Cache实现
  • caffeine cache

    2019-04-15 20:48:55
      guava cache的功能的确是很强大,满足了绝大多数的人的需求,但是其本质上还是LRU的一层封装,所以在众多其他较为优良的淘汰...而caffeine cache实现了W-TinyLFU(LFU+LRU算法的变种)。W-TinyLFU是命中率最接近...
  • Caffeine Cache实战

    2020-07-03 08:34:08
    Caffeine简介 Caffeine是基于JAVA 1.8 Version的高性能缓存库。Caffeine提供的内存缓存使用参考Google guava的API。Caffeine是基于Google Guava Cache设计经验上改进的成果。 并发测试 官方性能比较: ...
  • caffeine-2.8.5.jar

    2020-10-09 14:27:59
    Caffeine 是基于Java 8的高性能,接近最佳的缓存库。 Caffeine使用Google Guava启发的API提供内存缓存。 改进取决于您设计Guava缓存和ConcurrentLinkedHashMap的体验。
  • caffeine资料汇总

    2020-05-16 08:18:04
    1、springboot结合caffeineCaffeine Cache 进程缓存之王:https://www.jianshu.com/p/15d0a9ce37dd 和 Spring Boot缓存实战 Caffeine:https://www.jianshu.com/p/c72fb0c787fc 2、 新一代缓存-Caffeine:...
  • mac 不待机caffeine

    2017-10-26 11:01:59
    Caffeine for mac中文名字叫做——咖啡因,这是一款很有用的小工具。caffeine for mac可以让你的 Mac 屏幕保持点亮状态,不会因时间的问题进入休眠状态。

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 1,676
精华内容 670
关键字:

caffeine