进程缓存Caffeine

简介

  • 对于用户来说,响应的快慢是判断一个系统的重要指标,缓存就是必不可少的优化工具,在一个高并发的场景中往往占有着非常重要的角色,所以开发人员需要根据不同的应用场景来选择不同的缓存框架,比如分布式缓存redis,或者进程缓存GuavaCache。
  • 收存储的元素,而GuavaCache是一款非常优秀的进程缓存框架,很好的提供了读写和自动失效的功能。而今天要介绍的进程缓存Caffeine,在设计上参考了GuavaCache的经验,也进行了大量的改进优化,以下数据图片均来源于Caffeine GitHub地址:caffeine,首先是读写性能的比较:
  • 缓存和Map之间的一个根本区别是缓存会将储存的元素逐出。逐出策略决定了在什么时间应该删除哪些对象,逐出策略直接影响缓存的命中率,这是缓存库的关键特征。Caffeine使用Window TinyLfu逐出策略,该策略提供了接近最佳的命中率。
  • JVM缓存(堆缓存):创建全局变量,如Map,List等容器来存放数据。本文提到的Guava Cache、Ehcache、Caffeine都属于堆内存缓存。堆缓存只适用于单点使用,不适用分布式环境。

添加依赖

首先在pom.xml文件中添加Caffeine相关依赖:

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

您可以在Maven Central上找到最新版本的Caffeine。

缓存填充

让我们集中讨论Caffeine的三种缓存填充策略:手动,同步加载和异步加载。
首先,让我们创建一个用于存储到缓存中的

DataObject类:
class DataObject {
    private final String data;
 
    private static int objectCounter = 0;
    // standard constructors/getters
     
    public static DataObject get(String data) {
        objectCounter++;
        return new DataObject(data);
    }
}
  • 手动填充
    在这种策略中,我们手动将值插入缓存中,并在后面检索它们。
    让我们初始化缓存:
Cache<String, DataObject> cache = Caffeine.newBuilder()
  .expireAfterWrite(1, TimeUnit.MINUTES)
  .maximumSize(100)
  .build();

现在,我们可以使用getIfPresent方法从缓存中获取值。如果缓存中不存在该值,则此方法将返回null:

String key = "A";
DataObject dataObject = cache.getIfPresent(key);
 
assertNull(dataObject);
我们可以使用put方法手动将值插入缓存:
cache.put(key, dataObject);
dataObject = cache.getIfPresent(key);
 
assertNotNull(dataObject);

我们还可以使用get方法获取值,该方法将Lambda函数和键作为参数。如果缓存中不存在此键,则此Lambda函数将用于提供返回值,并且该返回值将在计算后插入缓存中:

dataObject = cache
  .get(key, k -> DataObject.get("Data for A"));
 
assertNotNull(dataObject);
assertEquals("Data for A", dataObject.getData());

get方法以原子方式(atomically)执行计算。这意味着计算将只进行一次,即使多个线程同时请求该值。这就是为什么使用get比getIfPresent更好。
有时我们需要手动使某些缓存的值无效:

cache.invalidate(key);
dataObject = cache.getIfPresent(key);
 
assertNull(dataObject);

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不可以同时使用

同步加载

这种加载缓存的方法具有一个函数,该函数用于初始化值,类似于手动策略的get方法。让我们看看如何使用它。
首先,我们需要初始化缓存:

LoadingCache<String, DataObject> cache = Caffeine.newBuilder()
  .maximumSize(100)
  .expireAfterWrite(1, TimeUnit.MINUTES)
  .build(k -> DataObject.get("Data for " + k));

现在,我们可以使用get方法检索值:

DataObject dataObject = cache.get(key);
 
assertNotNull(dataObject);
assertEquals("Data for " + key, dataObject.getData());
我们还可以使用getAll方法获得一组值:
Map<String, DataObject> dataObjectMap 
  = cache.getAll(Arrays.asList("A", "B", "C"));
 
assertEquals(3, dataObjectMap.size());

从传递给build方法的初始化函数中检索值。这样就可以通过缓存在来装饰访问值。

异步加载

该策略与先前的策略相同,但是异步执行操作,并返回保存实际值的CompletableFuture:

AsyncLoadingCache<String, DataObject> cache = Caffeine.newBuilder()
  .maximumSize(100)
  .expireAfterWrite(1, TimeUnit.MINUTES)
  .buildAsync(k -> DataObject.get("Data for " + k));

考虑到它们返回CompletableFuture的事实,我们可以以相同的方式使用get和getAll方法:

String key = "A";
 
cache.get(key).thenAccept(dataObject -> {
    assertNotNull(dataObject);
    assertEquals("Data for " + key, dataObject.getData());
});
 
cache.getAll(Arrays.asList("A", "B", "C"))
  .thenAccept(dataObjectMap -> assertEquals(3, dataObjectMap.size()));

CompletableFuture具有丰富而有用的API,您可以在本文中了解更多信息。

逐出元素

Caffeine具有三种元素逐出策略:基于容量,基于时间和基于引用。
基于容量的逐出
这种逐出发生在超过配置的缓存容量大小限制时。有两种获取容量当前占用量的方法,计算缓存中的对象数量或获取它们的权重。
让我们看看如何处理缓存中的对象。初始化高速缓存时,其大小等于零:

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");
cache.cleanUp();
 
assertEquals(1, cache.estimatedSize());

值得一提的是,在获取缓存大小之前,我们先调用cleanUp方法。这是因为缓存逐出是异步执行的,并且此方法有助于等待逐出操作的完成。
我们还可以传递一个weigher函数来指定缓存值的权重大小:

LoadingCache<String, DataObject> cache = Caffeine.newBuilder()
  .maximumWeight(10)
  .weigher((k,v) -> 5)
  .build(k -> DataObject.get("Data for " + k));
 
assertEquals(0, cache.estimatedSize());
 
cache.get("A");
assertEquals(1, cache.estimatedSize());
 
cache.get("B");
assertEquals(2, cache.estimatedSize());

当权重超过10时,将按照时间顺序从缓存中删除多余的值:

cache.get("C");
cache.cleanUp();
 
assertEquals(2, cache.estimatedSize());

基于时间的逐出
此逐出策略基于元素的到期时间,并具有三种类型:
• Expire after access — 自上次读取或写入发生以来,经过过期时间之后该元素到期。
• Expire after write — 自上次写入以来,在经过过期时间之后该元素过期。
• Custom policy — 通过Expiry实现分别计算每个元素的到期时间。
让我们使用expireAfterAccess方法配置访问后过期策略:

LoadingCache<String, DataObject> cache = Caffeine.newBuilder()
  .expireAfterAccess(5, TimeUnit.MINUTES)
  .build(k -> DataObject.get("Data for " + k));

要配置写后过期策略,我们使用expireAfterWrite方法:

cache = Caffeine.newBuilder()
  .expireAfterWrite(10, TimeUnit.SECONDS)
  .weakKeys()
  .weakValues()
  .build(k -> DataObject.get("Data for " + k));

要初始化自定义策略,我们需要实现Expiry接口:

cache = Caffeine.newBuilder().expireAfter(new Expiry<String, DataObject>() {
    @Override
    public long expireAfterCreate(
      String key, DataObject value, long currentTime) {
        return value.getData().length() * 1000;
    }
    @Override
    public long expireAfterUpdate(
      String key, DataObject value, long currentTime, long currentDuration) {
        return currentDuration;
    }
    @Override
    public long expireAfterRead(
      String key, DataObject value, long currentTime, long currentDuration) {
        return currentDuration;
    }
}).build(k -> DataObject.get("Data for " + k));
  • 基于引用的逐出
    • 我们可以将缓存配置为允许垃圾回收缓存的键或值。为此,我们将为键和值配置WeakRefence的用法,并且我们只能为值的垃圾收集配置为SoftReference。
      当对象没有任何强引用时,WeakRefence用法允许对对象进行垃圾回收。 SoftReference允许根据JVM的全局“最近最少使用”策略对对象进行垃圾收集。有关Java引用的更多详细信息,请参见此处。
      我们应该使用Caffeine.weakKeys(),Caffeine.weakValues()和Caffeine.softValues()来启用每个选项:
LoadingCache<String, DataObject> cache = Caffeine.newBuilder()
  .expireAfterWrite(10, TimeUnit.SECONDS)
  .weakKeys()
  .weakValues()
  .build(k -> DataObject.get("Data for " + k));
 
cache = Caffeine.newBuilder()
  .expireAfterWrite(10, TimeUnit.SECONDS)
  .softValues()
  .build(k -> DataObject.get("Data for " + k));

刷新缓存

可以将缓存配置为在定义的时间段后自动刷新元素。让我们看看如何使用refreshAfterWrite方法执行此操作:
Caffeine.newBuilder()
.refreshAfterWrite(1, TimeUnit.MINUTES)
.build(k -> DataObject.get("Data for " + k));
在这里,我们应该了解expireAfter和refreshAfter之间的区别。前者当请求过期元素时,执行将阻塞,直到build()计算出新值为止。
但是后者将返回旧值并异步计算出新值并插入缓存中,此时被刷新的元素的过期时间将重新开始计时计算。
统计
Caffeine可以记录有关缓存使用情况的统计信息:

LoadingCache<String, DataObject> cache = Caffeine.newBuilder()
  .maximumSize(100)
  .recordStats()
  .build(k -> DataObject.get("Data for " + k));
cache.get("A");
cache.get("A");
 
assertEquals(1, cache.stats().hitCount());
assertEquals(1, cache.stats().missCount());

我们将recordStats传递给它,recordStats创建StatsCounter的实现。每次与统计相关的更改都将推送给此对象。

### Caffeine 多级缓存的实现与优化 Caffeine 是一种高性能、轻量级的本地缓存库,适用于 Java 应用程序中的数据存储需求[^1]。当涉及到多级缓存架构时,通常会结合分布式缓存(如 Redis)和本地缓存(如 Caffeine)。这种设计能够显著减少远程调用延迟并提高系统的整体性能。 #### 一、多级缓存的设计原理 多级缓存的核心思想是在内存中维护多个层次的数据副本,从而降低对较慢存储介质(如数据库或分布式缓存)的访问频率。具体来说: - **第一层缓存**:通常是进程内的高速缓存,例如 Caffeine 或 GuavaCache。这类缓存的特点是没有网络开销且速度极快。 - **第二层缓存**:一般是分布式的共享缓存,比如 Redis 或 Memcached。它们适合处理跨服务实例之间的数据一致性问题。 通过这种方式,应用程序可以在最短时间内获取所需数据的同时保持较高的可用性和可靠性[^2]。 #### 二、Caffeine 的配置与集成 以下是使用 Spring Boot 集成 Caffeine 并构建两级缓存的一个简单示例: ```java import com.github.benmanes.caffeine.cache.Cache; import org.springframework.cache.annotation.Cacheable; import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Service; @Service public class CacheService { @Bean public Cache<String, Object> caffeineCache() { return com.github.benmanes.caffeine.cache.Caffeine.newBuilder() .maximumSize(100) // 设置最大容量为100条记录 .expireAfterWrite(5, TimeUnit.MINUTES) // 数据过期时间为5分钟 .build(); } @Cacheable(value = "local", cacheManager = "caffeineCacheManager") public String getDataFromLocal(String key) { System.out.println("Fetching data from local cache..."); return (String) caffeineCache().getIfPresent(key); } } ``` 上述代码片段展示了如何创建一个基本的 Caffeine 缓存实例,并将其作为一级缓存来管理热数据[^3]。 #### 三、多级缓存的最佳实践 为了更好地利用 Caffeine 和其他类型的缓存技术组合形成高效稳定的解决方案,请遵循以下建议: 1. **合理分配各级缓存的角色** - 将频繁使用的热点数据放入 L1(即 Caffeine),而较少请求或者较大的对象则可以考虑放到 L2(Redis)上。 2. **设置合适的淘汰策略** - 对于 Caffeine 来说,默认提供了多种驱逐算法供开发者选择,包括但不限于 `LRU` (Least Recently Used)、`LFU` (Least Frequently Used)以及时间戳驱动的方式等。这些机制可以帮助我们自动清理掉那些不再活跃或者是占用过多资源的对象。 3. **同步更新逻辑** - 当修改某个实体的状态之后,记得及时刷新所有的关联缓存位置,防止出现脏读现象。可以通过监听器模式或者其他异步通知手段完成这项工作。 4. **监控与调试工具的支持** - 使用专业的 APM(Application Performance Management) 工具定期审查整个链路的表现情况,找出瓶颈所在之处加以改进。 #### 四、总结 综上所述,采用 Caffeine 构建多层次缓存体系不仅可以极大地改善用户体验还能有效缓解后台服务器的压力。不过需要注意的是,在实际部署过程中还需要充分考虑到业务场景特点以及其他外部因素的影响,这样才能真正发挥出该方案的优势。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值