首页 > 其他分享 >Caffeine本地高性能缓存组件

Caffeine本地高性能缓存组件

时间:2025-01-06 23:11:09浏览次数:9  
标签:缓存 cache Caffeine user key 组件 元素

1. 简介

Caffeine是一个用于Java应用程序的高性能缓存框架。它提供了一个强大且易于使用的缓存库,可以在应用程序中使用,以提高数据访问的速度和效率。

下面是一些Caffeine缓存框架的主要特点:

  • 高性能:Caffeine的设计目标之一是提供卓越的性能。它通过使用高效的数据结构和优化的算法来实现快速的缓存访问。与其他一些常见的缓存框架相比,Caffeine在缓存访问的速度和响应时间上表现出色。

  • 内存管理:Caffeine提供了灵活的内存管理选项。它支持基于大小、基于数量和基于权重的缓存大小限制。你可以根据应用程序的需求来选择合适的缓存大小策略,并且可以通过配置参数进行进一步的调整。

  • 强大的功能:Caffeine提供了许多强大的功能来满足各种需求。它支持异步加载和刷新缓存项,可以设置过期时间和定时刷新策略,支持缓存项的自动删除和手动失效等。此外,Caffeine还提供了统计信息和监听器机制,可以方便地监控和管理缓存的状态和变化。

  • 线程安全:Caffeine是线程安全的,可以在多线程环境中安全地使用。它使用了细粒度的锁定机制来保护共享资源,确保并发访问的正确性和一致性。

  • 易于集成:Caffeine是一个独立的Java库,可以很容易地与现有的应用程序集成。它与标准的Java并发库和其他第三方库兼容,并且可以与各种框架和技术(如Spring、Hibernate等)无缝集成。

官方文档:https://github.com/ben-manes/caffeine/wiki/Home-zh-CN

2. Quick Start

写几个单元测试 熟悉一下 caffeine的基本用法

2.1 添加maven依赖

java8 最高只能使用2.x的版本

<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>2.9.3</version>
</dependency>

2.2 添加缓存

数据准备

private final List<User> users = Lists.newArrayList(
        new User(1, "zhangsan"),
        new User(2, "lisi"),
        new User(3, "wangwu"));

private final int userKey = 1;

private final List<Integer> userKeys = Lists.newArrayList(1, 2);

@SneakyThrows
private User getUserById(Integer id) {
    TimeUnit.SECONDS.sleep(1);
    return users.stream().filter(u -> Objects.equals(u.getId(), id)).findFirst().get();
}

2.2.1 手动加载

@Test
public void manual() {
    Cache<Integer, User> cache = Caffeine
            .newBuilder()
            // 元素写入10分钟后过期
            .expireAfterWrite(10, TimeUnit.MINUTES)
            // 最大能放1w个元素
            .maximumSize(10_000)
            .build();

    // 查找一个缓存元素, 没有查找到的时候返回null
    User user = cache.getIfPresent(userKey);
    // 如果缓存不存在则执行 mappingFunction 生成缓存元素返回, 并将元素put进cache
    // 类似于map的 computeIfAbsent方法
    user = cache.get(userKey, k -> getUserById(userKey));
    // 添加或者更新一个缓存元素
    cache.put(userKey, getUserById(userKey));
    // 移除一个缓存元素
    cache.invalidate(userKey);
}

推荐使用 cache.get(key, k -> value) 操作来在缓存中不存在该key对应的缓存元素的时候进行计算生成并直接写入至缓存内,而当该key对应的缓存元素存在的时候将会直接返回存在的缓存值。

2.2.2 自动加载

@Test
public void loading() {
    LoadingCache<Integer, User> cache = Caffeine
            .newBuilder()
            .maximumSize(10_000)
            .expireAfterWrite(10, TimeUnit.MINUTES)
            // 设置自动加载的function
            .build(this::getUserById);
    // 查找缓存,如果缓存不存在则自动调用getUserById生成缓存元素, 如果无法生成则返回null
    User user = cache.get(userKey);
    // 批量查找缓存,如果缓存不存在则生成缓存元素
    Map<Integer, User> users = cache.getAll(userKeys);
}

LoadingCache是一个Cache 附加上 CacheLoader能力之后的缓存实现。

通过 getAll可以达到批量查找缓存的目的。 默认情况下,在getAll 方法中,将会对每个不存在对应缓存的key调用一次 CacheLoader.load 来生成缓存元素。 在批量检索比单个查找更有效率的场景下,你可以覆盖并开发CacheLoader.loadAll 方法来使你的缓存更有效率。

2.2.3 异步手动加载

@Test
@SneakyThrows
public void asyncManual() {
    AsyncCache<Integer, User> cache = Caffeine
            .newBuilder()
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .maximumSize(10_000)
            // 构建异步对象
            .buildAsync();
    // 查找一个缓存元素, 没有查找到的时候返回null
    CompletableFuture<User> user = cache.getIfPresent(userKey);
    // 查找缓存元素,如果不存在,则异步生成
    user = cache.get(userKey, k -> getUserById(userKey));
    // 添加或者更新一个缓存元素
    cache.put(userKey, user);
    // 移除一个缓存元素
    cache.synchronous().invalidate(userKey);
}

AsyncCacheCache 的一个变体,AsyncCache提供了在 Executor上生成缓存元素并返回 CompletableFuture的能力。这给出了在当前流行的响应式编程模型中利用缓存的能力。

synchronous()方法给 Cache提供了阻塞直到异步缓存生成完毕的能力。

当然,也可以使用 AsyncCache.asMap()所暴露出来的ConcurrentMap的方法对缓存进行操作。

默认的线程池实现是 ForkJoinPool.commonPool() ,当然你也可以通过覆盖并实现 Caffeine.executor(Executor)方法来自定义你的线程池选择。

2.2.4 异步自动加载

@Test
@SneakyThrows
public void asyncLoading() {
    AsyncLoadingCache<Integer, User> cache = Caffeine
           .newBuilder()
           .maximumSize(10_000)
           .expireAfterWrite(10, TimeUnit.MINUTES)
           // 设置自动加载的function
           .buildAsync(key -> getUserById(key));
           // 也可以指定加载时使用缓存对象的executor
           //.buildAsync((key, executor) -> getUserById(key, executor));
    // 查找缓存元素,如果其不存在,将会异步进行生成
    CompletableFuture<User> user = cache.get(userKey);
    // 批量查找缓存元素,如果其不存在,将会异步进行生成
    CompletableFuture<Map<Integer, User>> users = cache.getAll(userKeys);
}

AsyncLoadingCache是一个 AsyncCache 加上 AsyncCacheLoader能力的实现。

2.3 缓存驱逐

Caffeine 提供了三种驱逐策略,分别是基于容量,基于时间和基于引用三种类型。

在默认情况下,当一个缓存元素过期的时候,Caffeine不会自动立即将其清理和驱逐。而它将会在写操作之后进行少量的维护工作,在写操作较少的情况下,也偶尔会在读操作之后进行。如果你的缓存吞吐量较高,那么你不用去担心你的缓存的过期维护问题。

2.3.1 基于容量

@Test
@SneakyThrows
public void evictionWithSize() {
    // 基于缓存内的元素个数进行驱逐
    LoadingCache<Integer, User> cacheWithSize = Caffeine.newBuilder()
          .maximumSize(1)
          .build(key -> getUserById(key));
    // 基于缓存内元素权重进行驱逐
    LoadingCache<Integer, User> cacheWitWeight = Caffeine.newBuilder()
          .maximumWeight(50)
          // 权重必须大于0
          .weigher((Integer key, User user) -> Math.abs(user.hashCode() % 100))
          .build(key -> getUserById(key));
    for (User user : users) {
        cacheWithSize.put(user.getId(), user);
        cacheWitWeight.put(user.getId(), user);
    }

    //因为是异步驱逐的 所以需要睡眠一下
    TimeUnit.SECONDS.sleep(1);
    log.info("cacheWithSize size:{}, element: {}", cacheWithSize.asMap().size(), cacheWithSize.asMap());
    // cacheWithSize size:1, element: {3=User(id=3, name=wangwu)}
    log.info("cacheWitWeight size:{} element: {}", cacheWitWeight.asMap().size(), cacheWitWeight.asMap());
    // cacheWitWeight size:2 element: {2=User(id=2, name=lisi), 3=User(id=3, name=wangwu)}
}

如果你的缓存容量不希望超过某个特定的大小,那么记得使用Caffeine.maximumSize(long)。缓存将会尝试通过基于就近度和频率的算法来驱逐掉不会再被使用到的元素。

另一种情况,你的缓存可能中的元素可能存在不同的“权重”--打个比方,你的缓存中的元素可能有不同的内存占用--你也许需要借助Caffeine.weigher(Weigher) 方法来界定每个元素的权重并通过 Caffeine.maximumWeight(long)方法来界定缓存中元素的总权重来实现上述的场景。除了“最大容量”所需要的注意事项,在基于权重驱逐的策略下,一个缓存元素的权重计算是在其创建和更新时,此后其权重值都是静态存在的,在两个元素之间进行权重的比较的时候,并不会根据进行相对权重的比较。

2.3.2 基于时间

@Test
public void evictionWithTime() {
    // 基于固定的过期时间驱逐策略 - 访问多久后过期
    LoadingCache<Integer, User> cacheWithAccessTime = Caffeine.newBuilder()
          .expireAfterAccess(5, TimeUnit.MINUTES)
          .build(key -> getUserById(key));
    // 基于固定的过期时间驱逐策略 - 写入多久后过期
    LoadingCache<Integer, User> cacheWithWriteTime = Caffeine.newBuilder()
          .expireAfterWrite(10, TimeUnit.MINUTES)
          .build(key -> getUserById(key));

    // 基于不同的过期驱逐策略
    LoadingCache<Integer, User> cacheWithDynamicTime = Caffeine.newBuilder()
          .expireAfter(new Expiry<Integer, User>() {
              // 创建多久后过期
              public long expireAfterCreate(Integer key, User user, long currentTime) {
                  // 给一个60-120秒的随机时间
                  return TimeUnit.SECONDS.toNanos(RandomUtils.nextInt(60, 120));
              }
              // 更新多久后过期
              public long expireAfterUpdate(Integer key, User graph,
                                            long currentTime, long currentDuration) {
                  return currentDuration;
              }
              // 访问多久后过期
              public long expireAfterRead(Integer key, User graph,
                                          long currentTime, long currentDuration) {
                  return currentDuration;
              }
          })
          .build(key -> getUserById(key));
}

Caffeine提供了三种方法进行基于时间的驱逐:

  • expireAfterAccess(long, TimeUnit): 一个元素在上一次读写操作后一段时间之后,在指定的时间后没有被再次访问将会被认定为过期项。
  • expireAfterWrite(long, TimeUnit): 一个元素将会在其创建或者最近一次被更新之后的一段时间后被认定为过期项。
  • expireAfter(Expiry): 一个元素将会在指定的时间后被认定为过期项。

在写操作,和偶尔的读操作中将会进行周期性的过期事件的执行。过期事件的调度和触发将会在O(1)的时间复杂度内完成。

2.3.3 基于引用

@Test
public void evictionWithReference() {
    // 当key和缓存元素都不再存在其他强引用的时候驱逐
    LoadingCache<Integer, User> cacheWithWeak = Caffeine.newBuilder()
          .weakKeys()
          .weakValues()
          .build(key -> getUserById(key));
    // 当进行GC的时候进行驱逐
    LoadingCache<Integer, User> cacheWithSoft = Caffeine.newBuilder()
          .softValues()
          .build(key -> getUserById(key));
}

Caffeine 允许你配置你的缓存去让GC去帮助清理缓存当中的元素,其中key支持弱引用,而value则支持弱引用和软引用。记住 AsyncCache不支持软引用和弱引用。

Caffeine.weakKeys() 在保存key的时候将会进行弱引用。这允许在GC的过程中,当key没有被任何强引用指向的时候去将缓存元素回收。由于GC只依赖于引用相等性。这导致在这个情况下,缓存将会通过引用相等(==)而不是对象相等 equals()去进行key之间的比较。

Caffeine.weakValues()在保存value的时候将会使用弱引用。这允许在GC的过程中,当value没有被任何强引用指向的时候去将缓存元素回收。由于GC只依赖于引用相等性。这导致在这个情况下,缓存将会通过引用相等(==)而不是对象相等 equals()去进行value之间的比较。

Caffeine.softValues()在保存value的时候将会使用软引用。为了相应内存的需要,在GC过程中被软引用的对象将会被通过LRU算法回收。由于使用软引用可能会影响整体性能,我们还是建议通过使用基于缓存容量的驱逐策略代替软引用的使用。同样的,使用 softValues() 将会通过引用相等(==)而不是对象相等 equals()去进行value之间的比较。

2.4 删除缓存

术语:

  • 驱逐(eviction) 缓存元素因为策略被移除(如2.3章节)
  • 失效(invalidation) 缓存元素被手动移除
  • 移除(removal) 由于驱逐或者失效而最终导致的结果
@Test
@SneakyThrows
public void removeOrEvictionRecord() {
    Cache<Integer, User> cache = Caffeine.newBuilder()
           .expireAfterWrite(5, TimeUnit.SECONDS)
           .evictionListener((Integer key, User user, RemovalCause cause) ->
                   log.info("Key {} was evicted ({})", key, cause))
           .removalListener((Integer key, User user, RemovalCause cause) ->
                   log.info("Key {} was removed ({})", key, cause))
           .build();
    for (User user : users) {
        cache.put(user.getId(), user);
    }

    log.info("cache data put success");
    TimeUnit.SECONDS.sleep(10);
    // 失效key
    cache.invalidate(userKey);
    // 批量失效key
    cache.invalidateAll(userKeys);
    // 失效所有的key
    cache.invalidateAll();
}

你可以为你的缓存通过Caffeine.removalListener(RemovalListener)方法定义一个移除监听器在一个元素被移除的时候进行相应的操作。这些操作是使用 Executor异步执行的,其中默认的 Executor 实现是 ForkJoinPool.commonPool() 并且可以通过覆盖Caffeine.executor(Executor)方法自定义线程池的实现。

当移除之后的自定义操作必须要同步执行的时候,你需要使用 Caffeine.evictionListener(RemovalListener) 。这个监听器将在 RemovalCause.wasEvicted() 为 true 的时候被触发。为了移除操作能够明确生效, Cache.asMap() 提供了方法来执行原子操作。

记住任何在 RemovalListener中被抛出的异常将会被吞食。

2.5 刷新缓存

@Test
@SneakyThrows
public void refreshAfterWrite() {
    // 同时使用 expireAfterWrite refreshAfterWrite
    // 使一个元素在其被允许刷新但是没有被主动查询的时候,这个元素也会被视为过期(防止不活跃的数据常驻内存)
    LoadingCache<Integer, User> cache = Caffeine.newBuilder()
          .maximumSize(10_000)
          .expireAfterWrite(Duration.ofSeconds(10))
          /*
           * 1. 写入到达指定时间后刷新
           * 2. 不是到达时间直接刷新 而是标记为准备刷新 数据下次访问的时候才开始刷新
           * 3. 在刷新的时候如果查询缓存元素,那么直接返回旧值
           */
          .refreshAfterWrite(Duration.ofSeconds(3))
          .build(key -> getUserById(key));
}

刷新和驱逐并不相同。可以通过LoadingCache.refresh(K)方法,异步为key对应的缓存元素刷新一个新的值。与驱逐不同的是,在刷新的时候如果查询缓存元素,其旧值将仍被返回,直到该元素的刷新完毕后结束后才会返回刷新后的新值。

expireAfterWrite相反,refreshAfterWrite 将会使在写操作之后的一段时间后允许key对应的缓存元素进行刷新,但是只有在这个key被真正查询到的时候才会正式进行刷新操作。所以打个比方,你可以在同一个缓存中同时用到 refreshAfterWriteexpireAfterWrite ,这样缓存元素在被允许刷新的时候不会直接刷新使得过期时间被盲目重置。当一个元素在其被允许刷新但是没有被主动查询的时候,这个元素也会被视为过期。

2.6 统计

@Test
public void statistics() {
    Cache<Integer, User> cache = Caffeine.newBuilder()
                                       .maximumSize(10_000)
                                       .recordStats()
                                       .build();
    // hitRate 查询缓存的命中率
    // evictionCount 被驱逐的缓存数量
    // averageLoadPenalty 新值被载入的平均耗时
    log.info("cache stats:{}", cache.stats());
    // cache stats:CacheStats{hitCount=0, missCount=0, loadSuccessCount=0, loadFailureCount=0, totalLoadTime=0, evictionCount=0, evictionWeight=0}
}

3. 集成SpringBoot

集成 SpringBoot 有两种方式

  1. 将cache对象声明称SpringBean然后使用的时候注入进来直接操作

  2. 结合SpringCacheManagercaffeine注册到cache模块中,然后使用spring注解进行缓存操作

    cache 方面的注解主要有以下 5 个:

    @Cacheable【创建、查询缓存】:触发缓存入口(一般放在创建和获取的方法上,@Cacheable 注解会先查询是否已经有缓存。如果有,则直接从缓存中返回;如果没有,则会执行方法并返回结果缓存【返回方法返回 NULL,则不进行缓存】)
    @CachePut【更新缓存】:更新缓存且不影响方法执行(用于修改的方法上,该注解下的方法始终会被执行)
    @CacheEvict【删除缓存】:触发缓存的 eviction(用于删除的方法上)
    @Caching【组合缓存配置】:将多个缓存组合在一个方法上(该注解可以允许一个方法同时设置多个注解)
    @CacheConfig【类级别共享配置】:在类级别设置一些缓存相关的共同配置(与其它缓存配合使用),避免在每个缓存方法上重复配置相同的缓存属性

标签:缓存,cache,Caffeine,user,key,组件,元素
From: https://www.cnblogs.com/ludangxin/p/18656473

相关文章

  • 【Vue.js 组件化】高效组件管理与自动化实践指南
    文章目录摘要引言组件命名规范与组织结构命名规范目录组织依赖管理工具自动化组件文档生成构建自动引入和文档生成的组件化体系代码结构自动引入组件配置使用Storybook展示组件文档自动生成代码详解QA环节总结参考资料摘要在现代前端开发中,组件化管理是Vue.......
  • Tomcat 三大核心线程组件Acceptor、Poller 和 Executor&为什么tomcat 要把Acceptor单
    在Tomcat的架构中,Acceptor、Poller 和 Executor 是处理网络连接和请求的重要组件。 AcceptorAcceptor 是Tomcat中负责接受新连接的组件。它的主要职责包括:监听端口:Acceptor在线程中监听一个特定的端口,等待客户端连接请求。接受连接:当有新的连接请求到达时,Accepto......
  • 【S32DS项目实战系列】项目工程外设 之 C40_IP组件
    前言:前面新建了一个工程但是没有任何驱动配置,接下来就教大家配置一下外设驱动的C40_IP1,文件自定义管理在S32DSIDE安装的目录下面的找到一下头文件,并复制到自建的文件夹里面用于自己建立的RTD库当中,当然这个是为了方面进行文件管理2,C40_IP组件简单介绍一下C40_IP组件......
  • 蓝易云 - Nginx一网打尽:动静分离、压缩、缓存、黑白名单、跨域、高可用、性能优化教程
    Nginx是一款高性能的开源Web服务器和反向代理服务器,它具有丰富的功能和模块,可以实现动静分离、压缩、缓存、黑白名单、跨域、高可用和性能优化。以下是Nginx一网打尽的教程:动静分离:动静分离是将动态资源(如PHP、Python脚本)和静态资源(如图片、CSS、JavaScript文件)部署在不同的......
  • vue2大屏适配,公共组件,copy即用,超详细教程!
    1.创建内容包裹组件(封装公共组件)scale-screen.vue<!--大屏自适应容器组件--><template><divclass="screen-wrapper"ref="screenWrapper":style="wrapperStyle"><slot></slot></div></template><sc......
  • Vue+Elementui: el-select组件下拉数据量大,产生页面卡顿或崩溃,详细解决方案!
    1.安装插件npminstallvue-virtual-scroll-list2.封装组件1>新建文件夹VirtualSelect创建文件:index.vue,OptionNode.vue/VirtualSelect/index.vue<template><div><el-selectsize="mini"popper-class="virtualselect"......
  • Jinja与WTF form使用日期时间组件的解决方法
    在使用Flask与其模板引擎Jinja2结合WTFforms(Flask-WTF)时,集成日期时间选择组件可以提高表单的用户体验。下面是一种方法来实现日期和时间选择功能:1、问题背景在使用Jinja和WTFform来创建表单时,遇到一个问题:希望在表单中添加日期时间组件,但使用form.validate_o......
  • 【从零开始入门unity游戏开发之——unity篇08】unity6基础入门——Unity游戏对象和组
    文章目录一、Unity游戏对象和组件的本质1、开发游戏的本质2、万物之根本——空对象2.1什么是空对象?2.2创建空对象3、Unity游戏对象的本质4、transform组件4.1transform组件的重要性4.2修改transform的值4.4**Transform的作用**4.3重置transform的值5、总结二、......
  • 后台管理系统动态面包屑Breadcrumb组件的实现
    在后管理系统开发中,面包屑导航是一个非常常见的功能,通常是根据当前的url自动生成面包屑导航菜单,当跳转路由发生变化时,面包屑导航都会随之发生变化,即动态面包屑。要完成动态面包屑我们需要制作一个动态数组,数组中每个item都表示一个路由信息,在这里我们使用到route.match......
  • 大模型基础组件 - 位置编码
    为什么需要位置编码自注意力机制没有顺序感知:Transformer模型中的自注意力(Self-Attention)机制计算的是输入序列中所有位置的元素之间的关系,而这些关系仅仅依赖于元素的内容,而不考虑它们在序列中的顺序。例如,模型并不能自动地识别“第一个词”和“最后一个词”之间的顺序关系......