• Caffeine Cache 进程缓存利器


    1、前言

    缓存的使用可以大幅度提升用户的体验度,所以缓存就是必不可少的一个神器,在多线程高并发场景中往往是离不开cache的,需要根据不同的应用场景来选择需要的cache,比如分布式缓存redis、memcached,还有本地(进程内)缓存ehcache、GuavaCache、Caffeine。

    Guava Cache是基于LRU算法实现,支持多种缓存过期策略。而Caffeine就是一个比Guava Cache性能更高的缓存框架。

    2、比较

    Caffeine是使用Java8对Guava缓存的重写版本,在Spring Boot 2.0中将取代,基于LRU算法实现,支持多种缓存过期策略。

    3、如何使用

    public static void main(String[] args) {
          LoadingCache<String, String> build = CacheBuilder.newBuilder().initialCapacity(1).maximumSize(100).expireAfterWrite(1, TimeUnit.DAYS)
              .build(new CacheLoader<String, String>() {
                 //默认的数据加载实现,当调用get取值的时候,如果key没有对应的值,就调用这个方法进行加载
                 @Override
                 public String load(String key)  {
                      return "";
                 }
             });
    }
    

    参数方法

    • initialCapacity(1) 初始缓存长度为1
    • maximumSize(100) 最大长度为100
    • expireAfterWrite(1, TimeUnit.DAYS) 设置缓存策略在1天未写入过期缓存(后面讲缓存策略)

    4、过期策略

    在Caffeine中分为两种缓存,一个是有界缓存,一个是无界缓存,无界缓存不需要过期并且没有界限。在有界缓存中提供了三个过期API:

    • expireAfterWrite: 代表着写了之后多久过期;
    • expireAfterAccess: 代表着最后一次访问了之后多久过期;
    • expireAfter: 在expireAfter中需要自己实现Expiry接口,这个接口支持create,update,access了之后多久过期。注意!这个API和前面两个API视乎池的。这里和前面两个API不同的是,需要你告诉缓存框架,他应该在具体的某个时间过期,也就是通过前面的重写create,update,access的方法,获取具体的过期时间。

    5、更新策略

    何为更新策略?设定多长时间之后会自动刷新缓存。

    Caffeine提供了refreshAfterWrite()方法来让我们进行写后多久更新策略:

    LoadingCache<String, String> build = CacheBuilder.newBuilder().refreshAfterWrite(1, TimeUnit.DAYS)
       .build(new CacheLoader<String, String>() {
              @Override
              public String load(String key)  {
                 return "";
              }
        });
    }
    

    上面的代码我们需要建立一个CacheLodaer来进行刷新,这里是同步进行的,可以通过buildAsync方法进行异步构建。在实际业务中这里可以把我们代码中的mapper传入进去,进行数据源的刷新。

    但是实际使用中,你设置了一天刷新,但是一天后你发现缓存并没有刷新。这是因为必有在1天后这个缓存再次访问才能刷新,如果没人访问,那么永远也不会刷新。

    我们来看看自动刷新他是怎么做的呢?自动刷新只存在读操作之后,也就是我们afterRead()这个方法,其中有个方法叫refreshIfNeeded,他会根据你是同步还是异步然后进行刷新处理。

    6、填充策略(Population)

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

    6.1、手动加载(Manual)

    // 初始化缓存
    Cache<String, Object> manualCache = Caffeine.newBuilder()
               .expireAfterWrite(10, TimeUnit.MINUTES)
               .maximumSize(10_000)
               .build();
    
    String key = "name1";
    // 根据key查询一个缓存,如果没有返回NULL
    graph = manualCache.getIfPresent(key);
    // 如果缓存中不存在该键,createExpensiveGraph函数将用于提供回退值,该值在计算后插入缓存中
    graph = manualCache.get(key, k -> createExpensiveGraph(k));
    // 使用 put 方法手动填充缓存,如果以前有值就覆盖以前的值
    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进而对缓存进行一些更改。

    6.2 同步加载(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可能会同时加载该组的其余部分。

    6.3 异步加载(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) 方法来替换线程池。

    7、驱逐策略(eviction)

    缓存的驱逐策略是为了预测哪些数据在短期内最可能被再次用到,从而提升缓存的命中率。LRU策略或许是最流行的驱逐策略。但LRU通过历史数据来预测未来是局限的,它会认为最后到来的数据是最可能被再次访问的,从而给予它最高的优先级。

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

    7.1 基于大小(size-based)

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

    // 根据缓存的计数进行驱逐
    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));
    

    我们可以使用Caffeine.maximumSize(long)方法来指定缓存的最大容量。当缓存超出这个容量的时候,会使用Window TinyLfu策略来删除缓存。我们也可以使用权重的策略来进行驱逐,可以使用Caffeine.weigher(Weigher) 函数来指定权重,使用Caffeine.maximumWeight(long) 函数来指定缓存最大权重值。

    让我们看看如何计算缓存中的对象。当缓存初始化时,其大小等于零:

    LoadingCache<String, DataObject> cache = Caffeine.newBuilder()
                          .maximumSize(1)
                          .build(k -> DataObject.get("Data for " + k));    
    assertEquals(0, cache.estimatedSize()); 
    

    当我们添加一个值时,大小明显增加:

    cache.get("A");    
    assertEquals(1, cache.estimatedSize()); 
    

    我们可以将第二个值添加到缓存中,这导致第一个值被删除:

    cache.get("B"); 
    assertEquals(1, cache.estimatedSize()); 
    

    注意:maximumWeight与maximumSize不可以同时使用。

    7.2 基于时间(Time-based)

    // 基于固定的到期策略进行退出
    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));
    
    // 要初始化自定义策略,我们需要实现 Expiry 接口
    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));
    

    7.3 基于引用(reference-based)

    // 当key和value都没有引用时驱逐缓存
    LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
                                              .weakKeys()
                                              .weakValues()
                                              .build(key -> createExpensiveGraph(key));
    
    // 当垃圾收集器需要释放内存时驱逐
    LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
                                              .softValues()
                                              .build(key -> createExpensiveGraph(key));
    

    我们可以将缓存的驱逐配置成基于垃圾回收器。当没有任何对对象的强引用时,使用 WeakRefence 可以启用对象的垃圾收回收。SoftReference 允许对象根据 JVM 的全局最近最少使用(Least-Recently-Used)的策略进行垃圾回收。

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

    8. 移除监听器(Removal)

    如果我们需要在缓存被移除的时候,得到通知产生回调,并做一些额外处理工作。这个时候RemovalListener就派上用场了。

    8.1 概念

    驱逐(eviction):由于满足了某种驱逐策略,后台自动进行的删除操作
    无效(invalidation):表示由调用方手动删除缓存
    移除(removal):监听驱逐或无效操作的监听器
    手动删除缓存:在任何时候,您都可能明确地使缓存无效,而不用等待缓存被驱逐。

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

    8.2 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)并不会抛出。

    9、统计(Statistics)

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

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

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

    原文链接:https://www.jianshu.com/p/15d0a9ce37dd
    来源:简书

  • 相关阅读:
    sfs2x 连接 mongodb
    java websocket
    webstorm 4.0 注册码
    解决 sfs2 admin tool 找不到扩展
    window 注册表五大类
    opengl 学习第二日
    java google Protobuf
    扩展 java sencha touch PhonegapPlugin
    sencha touch2 kryonet socket phonegap 通信 作者:围城
    sencha touch2 layout 笔记
  • 原文地址:https://www.cnblogs.com/john1015/p/15906278.html
Copyright © 2020-2023  润新知