Java本地缓存框架系列-Caffeine-1. 简介与使用

简介: Java本地缓存框架系列-Caffeine-1. 简介与使用

Caffeine 是一个基于Java 8的高性能本地缓存框架,其结构和 Guava Cache 基本一样,api也一样,基本上很容易就能替换。 Caffeine 实际上就是在 Guava Cache 的基础上,利用了一些 Java 8 的新特性,提高了某些场景下的性能效率。

这一章节我们会从 Caffeine 的使用引入,并提出一些问题,之后分析其源代码解决这些问题来让我们更好的去了解 Caffeine 的原理,更好的使用与优化,并且会对于我们之后的编码有所裨益。

我们来看一下 Caffeine 的基本使用,首先是创建:


限制缓存大小


Caffeine 有两种方式限制缓存大小。两种配置互斥,不能同时配置

1. 创建一个限制容量 Cache

Cache<String, Object> cache = Caffeine
                            .newBuilder()
                            //设置缓存的 Entries 个数最多不超过1000个
                            .maximumSize(1000)
                            .build();

需要注意的是,实际实现上为了性能考虑,这个限制并不会很死板:

  • 在缓存元素个数快要达到最大限制的时候,过期策略就开始执行了,所以在达到最大容量前也许某些不太可能再次访问的 Entry 就被过期掉了。
  • 有时候因为过期 Entry 任务还没执行完,更多的 Entry 被放入缓存,导致缓存的 Entry 个数短暂超过了这个限制

配置了 maximumSize 就不能配置下面的 maximumWeight 和 weigher


2. 创建一个自定义权重限制容量的 Cache

Cache<String, List<Object>> stringListCache = Caffeine.newBuilder()
    //最大weight值,当所有entry的weight和快达到这个限制的时候会发生缓存过期,剔除一些缓存
    .maximumWeight(1000)
    //每个 Entry 的 weight 值
    .weigher(new Weigher<String, List<Object>>() {
        @Override
        public @NonNegative int weigh(@NonNull String key, @NonNull List<Object> value) {
            return value.size();
        }
    })
    .build();

当你的缓存的 Key 或者 Value 比较大的时候,想灵活地控制缓存大小,可以使用这种方式。上面我们的 key 是一个 list,以 list 的大小作为 Entry 的大小。 当把 Weigher 实现为只返回1,maximumWeight 其实和 maximumSize 是等效的。 同样的,为了性能考虑,这个限制也不会很死板。

在这里,我们提出第一个问题:Entry是怎么保存,怎么过期的呢?


3. 指定初始大小

Cache<String, Object> cache = Caffeine.newBuilder()
    //指定初始大小
    .initialCapacity(1000)
    .build();

HashMap类似,通过指定一个初始大小,减少扩容带来的性能损耗。这个值也不宜过大,浪费内存。

在这里,我们提出第二个问题:这个初始大小,影响那些存储参数呢?


4. 指定Key, Value为非强引用类型

Cache<String, Object> cache = Caffeine.newBuilder()
    // 设置 key 为 WeakReference
    .weakKeys()
    .build();
cache = Caffeine.newBuilder()
    // 设置 key 为 WeakReference
    .weakKeys()
    // 设置 value 为 WeakReference
    .weakValues()
    .build();
cache = Caffeine.newBuilder()
    // 设置 key 为 WeakReference
    .weakKeys()
    // 设置 value 为 SofReference
    .softValues()
    .build();

对于 Java 中的 StrongReference,WeakReference,SoftReference,可以参考我的另外一篇文章:JDK核心JAVA源码解析(3) - 引用相关 在这里简单归纳下:

  1. StrongReference:强引用就是指在程序代码之中普遍存在的,一般的new一个对象并赋值给一个对象变量,就是一个强引用;只要某个对象有强引用与之关联,JVM必定不会回收这个对象,即使在内存不足的情况下,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。
  2. SoftReference:软引用是用来描述一些有用但并不是必需的对象,在Java中用java.lang.ref.SoftReference类来表示。。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。
  3. WeakReference:用来描述非必须的对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示

Caffeine 中的 Key,可以是 WeakReference,但是目前不能指定为 SoftReference,所以我们在这里提出第三个问题,为什么 Key 不能指定为 SoftReference,SoftReference 为何被区别对待

设置 Key 和 Value 的 Reference 类型,也是一种限制大小的方式,但是限制比较多:

  • 使用 weakKeys 就不能使用 Writer (这里提出第四个问题,为什么 weakKeys 和 Writer 不能同时使用
  • 使用 weakValues 或者 softValues 就不能使用异步缓存 buildAsync(这里提出第五个问题,为什么使用 weakValues 或者 softValues 就不能使用异步缓存

一般通过 maximumSize 还有 maximumWeight 就能满足我们的需求。


设置过期时间相关


1. 自定义过期

Cache<String, Order> cache = Caffeine.newBuilder()
    .expireAfter(new Expiry<String, Order>() {
        @Override
        //设置 Entry 创建后的过期时间
        //这里设置为 60s 后过期
        public long expireAfterCreate(@NonNull String key, @NonNull Order value, long currentTime) {
            return 1000 * 1000 * 1000 * 60;
        }
        @Override
        //设置 Entry 更新后的过期时间
        //这里返回 currentDuration 表示永远不过期
        public long expireAfterUpdate(@NonNull String key, @NonNull Order value, long currentTime, @NonNegative long currentDuration) {
            return currentDuration;
        }
        @Override
        //设置 Entry 读取后的过期时间
        //这里设置为 Order 的 createTime 的 60s 后过期
        public long expireAfterRead(@NonNull String key, @NonNull Order value, long currentTime, @NonNegative long currentDuration) {
            return 1000 * 1000 * 1000 * 60 - (System.currentTimeMillis() - value.createTime()) * 1000;
        }
    })
    .build();

通过实现 Expiry 接口,设置过期策略。这个接口主要包括三个值:

  • Entry 创建后的过期时间:参数为 Entry 的 Key 还有 Value,以及 Entry 创建时间。需要返回的是这个 Entry 的生育过期时间,单位是 nanoSeconds
  • Entry 更新后的过期时间:参数为 Entry 的 Key 还有 Value,以及当前时间(并不是系统当前时间,而是 Ticker 里面的当前时间,如果需要获取系统当前时间需要自己手动获取)和当前剩余的过期时间。需要返回的是这个 Entry 的剩余过期时间,单位是 nanoSeconds。如果永远不过期,可以返回 currentDuration 表示剩余时间永远不变,永远不过期。
  • Entry 读取后的过期时间:参数为 Entry 的 Key 还有 Value,以及当前时间(并不是系统当前时间,而是 Ticker 里面的当前时间,如果需要获取系统当前时间需要自己手动获取)和当前剩余的过期时间。需要返回的是这个 Entry 的剩余时间,单位是 nanoSeconds。如果永远不过期,可以返回 currentDuration 表示剩余时间永远不变,永远不过期。

这个配置与接下来的 expireAfterWrite 和 expireAfterAccess 互斥。不能同时配置

** 2. 设置写入以及更新后过期**

Cache<String, Object> cache = Caffeine.newBuilder()
    //写入或者更新1分钟后,缓存过期并失效
    .expireAfterWrite(1, TimeUnit.MINUTES)
    .build();

这个配置与上面的 expireAfter 互斥,不能同时配置

** 3. 设置操作后过期**

Cache<String, Object> cache = Caffeine.newBuilder()
    //写入或者更新或者读取1分钟后,缓存过期并失效
    .expireAfterAccess(1, TimeUnit.MINUTES)
    .build();

这个配置与上面的 expireAfter 互斥,不能同时配置


LoadingCache 相关


** 1. 生成LoadingCache **

Cache<String, Object> cache = Caffeine.newBuilder()
    //使用 CacheLoader 初始化
    .build(key -> {
        return loadFromDB(key);
    });

当 Key 不存在或者已过期时,会调用 CacheLoader 重新加载这个 Key。那么,这里要提出下面这些问题:

  1. Key 是否可以为 Null,为什么
  2. 调用 CacheLoader 的时候,如果有异常会怎样


2. 设置定时重新加载时间

Cache<String, Object> cache = Caffeine.newBuilder()
    //设置在写入或者更新之后1分钟后,调用 CacheLoader 重新加载
    .refreshAfterWrite(1, TimeUnit.MINUTES)
    //使用 CacheLoader 初始化
    .build(key -> {
        return loadFromDB(key);
    });

注意设置了这个配置,就只能通过build(CacheLoader)来生成 LoadingCache,不能生成普通的 Cache 了


额外配置


1. 统计记录相关

Cache<String, Object> cache = Caffeine.newBuilder()
    //打开数据采集
    .recordStats().build();
Cache<String, Object> cache = Caffeine.newBuilder()
    //自定义数据采集器
    .recordStats(() -> new StatsCounter() {
        @Override
        public void recordHits(@NonNegative int count) {
        }
        @Override
        public void recordMisses(@NonNegative int count) {
        }
        @Override
        public void recordLoadSuccess(@NonNegative long loadTime) {
        }
        @Override
        public void recordLoadFailure(@NonNegative long loadTime) {
        }
        @Override
        public void recordEviction() {
        }
        @Override
        public void recordEviction(@NonNegative int weight) {
        }
        @Override
        public void recordEviction(@NonNegative int weight, RemovalCause cause) {
        }
        @Override
        public @NonNull CacheStats snapshot() {
            return null;
        }
}).build();

这里我们提出两个问题:

  1. 默认的数据采集是否会影响性能
  2. 数据采集都会采集哪些数据

2. 某个 Entry 过期被移除后的回调

Cache<String, Object> cache = Caffeine
    .newBuilder()
    .removalListener((key, value, cause) -> {
        log.info("{}, {}, {}", key, value, cause);
    })
    .build();

回调里面有三个参数,包括 Entry 的 Key, Entry 的 Value 以及移除原因 cause。这个原因是一个枚举类型:

public enum RemovalCause {
    EXPLICIT {
        @Override public boolean wasEvicted() {
          return false;
        }
    },
    REPLACED {
        @Override public boolean wasEvicted() {
          return false;
        }
    },
    COLLECTED {
        @Override
        public boolean wasEvicted() {
            return true;
        }
    },
    EXPIRED {
        @Override
        public boolean wasEvicted() {
            return true;
        }
    },
    SIZE {
        @Override
        public boolean wasEvicted() {
            return true;
        }
    };
}

这里再提出一个问题:失效原因究竟对应哪些 API 的操作导致的失效?


3. 缓存主动更新其他存储或者资源

我们还可以通过设置 Writer,将对于缓存的更新,作用于其他存储,例如数据库:

Cache<String, Object> cache = Caffeine.newBuilder()
    .writer(new CacheWriter<String, Object>() {
        @Override
        public void write(@NonNull String key, @NonNull Object value) {
            //缓存更新时(包括创建和修改,不包括load),回调这里
            //数据库更新
            db.upsert(key, value);
        }
        @Override
        public void delete(@NonNull String key, @Nullable Object value, @NonNull RemovalCause cause) {
            //缓存失效时(包括任何原因的失效),回调这里
            //数据库更新
            db.markAsDeleted(key, value);
        }
    })
    .build();

那么就引出了如下几个问题:

  • 如果回调发生异常,会怎么处理?
  • 具体哪些 API 会引发 write,哪些会引发 delete


异步缓存


1. 生成异步缓存

AsyncCache<String, Object> cache = Caffeine.newBuilder()
    //生成异步缓存
    .buildAsync();

这种缓存,获取的 Value 都是一个 CompletableFuture

**2. 生成异步 LoadingCache **

AsyncCache<String, Object> cache = Caffeine.newBuilder()
    //生成异步缓存
    .buildAsync(key -> {
        return loadFromDB(key);
    });


3. 设置异步任务线程池

AsyncCache<String, Object> cache = Caffeine.newBuilder()
    .executor(new ForkJoinPool(10))
    //生成异步缓存
    .buildAsync();

这里我们提出如下问题:

  1. 异步缓存里面,哪些操作是异步的?
  2. 这些异步任务,执行的线程池默认是哪个?
  3. 异步任务有异常,如何处理?

到这里我们基本把创建说完了,接下来看一下使用这些缓存:

Cache<String, String> syncCache = Caffeine.newBuilder().build();
//加入缓存
syncCache.put(key, value);
//批量加入
syncCache.putAll(keyValueMap);
//读取缓存,如果不存在,则执行后面的mappingFunction读取并放入缓存
syncCache.get(key, k -> {
    return readFromOther(k);
});
//批量读取
syncCache.getAll(keys, ks -> {
   return readFromOther(k);
});
//获取缓存配置信息,以及其他维度的信息
Policy<String, String> policy = syncCache.policy();
//获取统计信息,前提是必须打开统计
CacheStats stats = syncCache.stats();
//获取某个key,如果不存在则返回null
syncCache.getIfPresent(key);
//将map转换为map,对map的修改会影响缓存
ConcurrentMap<@NonNull String, @NonNull String> map = syncCache.asMap();
//让某个key生效
syncCache.invalidate(key);
//让所有key失效
syncCache.invalidateAll();
//批量失效
syncCache.invalidateAll(keys);
//估计大小
@NonNegative long estimatedSize = syncCache.estimatedSize();
//等待过期清理任务完成,让缓存处于一个稳定状态
syncCache.cleanUp();

这里只提了同步缓存,异步缓存的 API 类似,只是取值变成了 CompletableFuture 包装的

接下来的章节,我们会深入研究 Caffeine 的源代码和实现原理及思想

相关文章
|
2月前
|
Java 数据库
在Java中使用Seata框架实现分布式事务的详细步骤
通过以上步骤,利用 Seata 框架可以实现较为简单的分布式事务处理。在实际应用中,还需要根据具体业务需求进行更详细的配置和处理。同时,要注意处理各种异常情况,以确保分布式事务的正确执行。
|
11天前
|
存储 安全 Java
Java 集合框架中的老炮与新秀:HashTable 和 HashMap 谁更胜一筹?
嗨,大家好,我是技术伙伴小米。今天通过讲故事的方式,详细介绍 Java 中 HashMap 和 HashTable 的区别。从版本、线程安全、null 值支持、性能及迭代器行为等方面对比,帮助你轻松应对面试中的经典问题。HashMap 更高效灵活,适合单线程或需手动处理线程安全的场景;HashTable 较古老,线程安全但性能不佳。现代项目推荐使用 ConcurrentHashMap。关注我的公众号“软件求生”,获取更多技术干货!
33 3
|
2月前
|
消息中间件 Java Kafka
在Java中实现分布式事务的常用框架和方法
总之,选择合适的分布式事务框架和方法需要综合考虑业务需求、性能、复杂度等因素。不同的框架和方法都有其特点和适用场景,需要根据具体情况进行评估和选择。同时,随着技术的不断发展,分布式事务的解决方案也在不断更新和完善,以更好地满足业务的需求。你还可以进一步深入研究和了解这些框架和方法,以便在实际应用中更好地实现分布式事务管理。
|
2月前
|
JSON Java Apache
非常实用的Http应用框架,杜绝Java Http 接口对接繁琐编程
UniHttp 是一个声明式的 HTTP 接口对接框架,帮助开发者快速对接第三方 HTTP 接口。通过 @HttpApi 注解定义接口,使用 @GetHttpInterface 和 @PostHttpInterface 等注解配置请求方法和参数。支持自定义代理逻辑、全局请求参数、错误处理和连接池配置,提高代码的内聚性和可读性。
189 3
|
2月前
|
缓存 NoSQL Java
什么是缓存?如何在 Spring Boot 中使用缓存框架
什么是缓存?如何在 Spring Boot 中使用缓存框架
73 0
|
11天前
|
存储 缓存 自然语言处理
SCOPE:面向大语言模型长序列生成的双阶段KV缓存优化框架
KV缓存是大语言模型(LLM)处理长文本的关键性能瓶颈,现有研究多聚焦于预填充阶段优化,忽视了解码阶段的重要性。本文提出SCOPE框架,通过分离预填充与解码阶段的KV缓存策略,实现高效管理。SCOPE保留预填充阶段的关键信息,并在解码阶段引入滑动窗口等策略,确保重要特征的有效选取。实验表明,SCOPE仅用35%原始内存即可达到接近完整缓存的性能水平,显著提升了长文本生成任务的效率和准确性。
30 3
SCOPE:面向大语言模型长序列生成的双阶段KV缓存优化框架
|
3天前
|
自然语言处理 Java 关系型数据库
Java mysql根据很长的富文本如何自动获取简介
通过使用Jsoup解析富文本并提取纯文本,然后根据需要生成简介,可以有效地处理和展示长文本内容。该方法简单高效,适用于各种应用场景。希望本文对您在Java中处理富文本并生成简介的需求提供实用的指导和帮助。
31 14
|
4天前
|
自然语言处理 Java 关系型数据库
Java mysql根据很长的富文本如何自动获取简介
通过使用Jsoup解析富文本并提取纯文本,然后根据需要生成简介,可以有效地处理和展示长文本内容。该方法简单高效,适用于各种应用场景。希望本文对您在Java中处理富文本并生成简介的需求提供实用的指导和帮助。
25 9
|
1月前
|
存储 JavaScript Java
Java 中的 String Pool 简介
本文介绍了 Java 中 String 对象及其存储机制 String Pool 的基本概念,包括字符串引用、构造方法中的内存分配、字符串文字与对象的区别、手工引用、垃圾清理、性能优化,以及 Java 9 中的压缩字符串特性。文章详细解析了 String 对象的初始化、内存使用及优化方法,帮助开发者更好地理解和使用 Java 中的字符串。
Java 中的 String Pool 简介
|
2月前
|
存储 缓存 安全
Java 集合框架优化:从基础到高级应用
《Java集合框架优化:从基础到高级应用》深入解析Java集合框架的核心原理与优化技巧,涵盖列表、集合、映射等常用数据结构,结合实际案例,指导开发者高效使用和优化Java集合。
48 4