专业的JAVA编程教程与资源

网站首页 > java教程 正文

追求极致性能,Java高速缓存 Caffeine

temp10 2025-01-01 22:47:21 java教程 13 ℃ 0 评论

本文介绍基于企业应用和微服务系统的常用的 Java 开源工具类,这类工具功能强大、性能良好,在项目中应用可提高系统性能,增强代码可维护性。本文以高速缓存 Caffeine为例简要分析开源工具,了解开源工具是如何把性能提高到极致的。

高速缓存 Caffeine

追求极致性能,Java高速缓存 Caffeine

说到缓存,就要说一下现代计算机 CPU 的组成CPU 除中央处理器外还有一级缓存和二级缓存,甚至三级缓存;在 CPU 中,缓存的作用是弥补低速外部存储和高速处理的 CPU 之间不匹配的缺陷。

在现代系统中,我们面对的是高并发快速响应的需求目标,但一直横亘在我们面前的难题是DataBase的速度不能大幅度地提升,也就无法实现目标的快速响应。

借鉴现代计算机结构中的解决办法,在系统中开始引入缓存。

Caffeine 的主要作者Ben Manes,Ben 是 Google 的前成员,也是 ConcurrentLinkedHashMap的数据结构作者。

Caffeine 的开发原因是 Ben 想用 Java 8 重写 Guava Cache 库,因此 API 在设计上与 Guava Cache 几乎一致,并且提供了 Guava Cache 的适配器,使得 Guava Cache 可以使Caffeine,从而极为平滑地从 Guava Cache 迁移至 Caffeine。

虽然说是重写了 Guava Cache,但与 Guava Cache 使用了不同的设计,Caffeine 使用了更先进的算法(Window-TinyLFU)和更优秀的数据结构(Striped Ring Buffer、TimeWheel),带来了极为灵活的配置、超强的性能,以及高命中率(最接近 Optimal 的命中率)。

Caffeine 不是一个分布式缓存,也不支持持久化。那为什么依然要使用它呢?

因为在一个系统中,我们设计缓存部分的时候,为了获得更好的性能和稳定性,不能只考虑分布式缓存,还应该考虑用 Caffeine 实现一级缓存,甚至是虚拟机内的多级缓存。

一、安装 Caffeine

在 Pom 中加入以下配置即可使用 Caffeine:

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

   <!--加入 guava-testlib 是为了在时间淘汰策略中配置基准时钟,而不采用系统时钟-->:

   <dependency>
         <groupId>com.google.guava</groupId>
         <artifactId>guava-testlib</artifactId>
         <version>28.0-jre</version>
         <scope>compile</scope>
       </dependency>

Caffenine 目前的最新版本是 3.1.1适用于 JDK 11,本书以 JDK 8 为主,所以使用 2.x 版本。Caffeine 的缓存架构在 3.x 版本中并没有变化。

二、Caffeine 的基本使用方法

Caffeine 作为虚拟机内缓存,使用方式相对简单,它支持手动填充同步自动载入异步手动填充异步自动载入四种填充数据的办法。获得一个同步载入的 Cache:

//manualLoads
Cache<String, SkuInfo> cache = Caffeine.newBuilder()
    .expireAfterWrite(1, TimeUnit.MINUTES)
    .maximumSize(100)
    .build();
String key = "11757834";
SkuInfo skuInfo = new SkuInfo(key);
cache.put(key, skuInfo);
//取出缓存对象,如果没有,则返回空
SkuInfo cachedSkuInfo = cache.getIfPresent(key);
//移出 Key
cache.invalidate(key);
skuInfo = cache.get(key, k -> service.query(k));

以上创建了一个最大容量为 100 个、写过期是 1 分钟的缓存。这里的写过期是指如果缓存项写入或被替换超过 1 分钟,则自动被删除。缓存的管理是 Caffeine 的重点,将在后面详细介绍。

SkuInfo 是需要缓存的对象,定义如下:

public class SkuInfo {
      private final String key;
      private String name ;
      public SkuInfo(String key) {
             this.key = key;
             this.name = "商品 "+key;
       }
       @Override
       public String toString() {
              return "SkuInfo{" + "key='" + key + '\'' + ", name='" + name + '\'' + '}';
       }
}    

手动填充数据就是由用户明确地控制数据的获取、更新和移除。实体可以直接使用cache.put(key, value)方法写入缓存,如果是用于更新操作,那么会覆盖原有的相同 Key 的实体。

推荐使用 cache.get(key, k -> value)方法获取值,可以原子性计算并插入缓存,避免与写操作竞争。

可以使用 cache.invalidate(key)方法使数据立刻从 Cache 中移除,而不用等待 Caffeine 在维护周期内清理不可用的 Value。

大多数场景下使用自动载入方式,以下代码是建立一个自动载入缓存 LoadingCache:

//synchronizeLoading
LoadingCache<String, SkuInfo> cache = Caffeine.newBuilder()
    .maximumSize(2)
    .expireAfterWrite(1, TimeUnit.MINUTES)
     //同步加载
     .build( k -> service.query(k));
String key = "11757834";
SkuInfo skuInfo = cache.get(key);
System.out.println(skuInfo);

异步手动填充是当数据不在缓存中时,使用 Future 异步填充数据。与手动填充的区别在于,get 方法返回的不是 Value,而是 CompletableFuture,等待异步执行完成后,交由用户自己做一些后续的处理。

以下代码是获得一个 AsyncCache:

AsyncCache<String, SkuInfo> cache = Caffeine.newBuilder()
    .maximumSize(100)
    .expireAfterWrite(1, TimeUnit.MINUTES)
    .buildAsync();
String key = "11757834";
CompletableFuture<SkuInfo> completableFuture = cache.get(key,k -> service.query(k));
completableFuture.thenAccept(skuInfo -> {
    System.out.println(skuInfo);;
});

异步自动载入 AsyncLoadingCache 的 API 与手动自动载入的 API 很相似,buildAsync 可选传入 CacheLoader 和 AsyncCacheLoader。如果希望直接同步返回值,那么传入 CacheLoader;如果希望异步返回值,则传入 AsyncCacheLoader。

可以在构建 Cache 时使用 Caffeine.executor方法指定一个线程池,默认的线程执行使用的是 ForkJoinPool.commonPool(),实际项目建议自定义一个线程池便于管理。

//asyncLoading
AsyncLoadingCache<String, SkuInfo> cache = Caffeine.newBuilder()
                .maximumSize(100)
                .expireAfterWrite(1, TimeUnit.MINUTES)
                .executor(pool)
                .buildAsync(k -> service.query(k));

       String key = "11757834";

       CompletableFuture<SkuInfo> completableFuture = cache.get(key);
        completableFuture.thenAccept(skuInfo -> {
                 System.out.println(skuInfo);
        });

如果想将异步缓存转为同步调用,则可以调用 cache.synchronous()方法,返回一个映射相同Cache 的 LoadingCache 同步视图,任何在此视图上的操作,都会映射至底层 Cache,如果操作被阻塞,那么会一直等待阻塞结束才继续进行。

cache.asMap()方法返回一个映射相同 Cache 的 ConcurrentMap 视图,任何一种 Cache 都具有此方法。返回的视图是线程安全的,并且保持和底层 Cache 的弱一致性,任何对此视图的操作和修改都会反映到 Cache 中;对视图进行迭代时,如果对底层缓存进行诸如驱逐/删除等操作,那么将无法反映在视图上。


内容摘自《高性能Java系统权威指南》第五章

本书特点:

内容上,总结作者从事Java开发20年来在头部IT企业的高并发系统经历的真实案例,极具参考意义和可读性。

对于程序员和架构师而言,Java 系统的性能优化是一个超常规的挑战。这是因为 Java 语言和 Java 运行平台,以及 Java 生态的复杂性决定了 Java 系统的性能优化不再是简单的升级配置或者简单的 “空间换时间”的技术实现,这涉及 Java 的各种知识点。

本书从高性能、易维护、代码增强以及在微服务系统中编写Java代码的角度来描述如何实现高性能Java系统,结合真实案例,让读者能够快速上手实战。

风格上本书的风格偏实战,读者可以下载书中的示例代码并运行测试。读者可以从任意一章开始阅读,掌握性能优化知识为公司的系统所用。

本书适合:

中高级程序员和架构师;

以及有志从事基础技术研发、开源工具研发的极客阅读;

也可以作为 Java 笔试和面试的参考书。

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表