Caffeine针对ConcurrentHashMap做了哪些改进?Window TinyLFU算法详解

Caffeine针对ConcurrentHashMap进行的改进

优化角度

  1. 缓冲化:使用写入和访问记录缓冲区,将零散的、并发的操作变为批量的、顺序的操作。

  2. 异步化:将维护任务从主读写路径中剥离,交给后台线程处理

  3. 处置化:将经常变化的状态(访问顺序、访问频率)从主存储结构剥离(CHM 的 Value),使用更高效的专业数据结构(时间轮、Count-Min Sketch)管理

  4. 无锁化:在核心读路径上,使用volatile读和无锁操作,确保性能不受影响

一、写缓冲区

作用:使用写入和访问记录缓冲区,将零散的、并发的操作变为批量的、顺序的操作。

  1. 当有写入操作(putremove)发生时,Caffeine 并不立即直接操作底层的 CHM。

  2. 而是先将这个操作(以及相关的元数据变更,如访问记录)追加到一个无锁的多生产者单消费者队列中。

  3. 由一个独立的后台线程(或在你调用 get 时顺带处理)批量地从缓冲区中取出多个操作,再批量地应用到真正的 CHM 上。

好处

  • 减少锁竞争:将高并发下大量零散的、需要竞争锁的写操作,合并成少量的、批量的写操作,极大地降低了 CHM 的锁粒度。

  • 提升响应速度:写入线程只需将操作放入缓冲区即可返回,无需等待真正的 CHM 操作完成,写入延迟更低。

二、锁策略

Caffeine 极力避免在常见的读路径上进行任何需要锁的操作。

  • Guava Cache 的对比:Guava Cache 为了实现 LRU,每次 get 访问都需要获取锁来操作访问队列(移动链表节点)。

  • Caffeine 的做法

    • 无锁的读:基础的 get 操作就是 CHM 的 get,这本身几乎是无锁的(或使用非常高效的 volatile 读和 CAS)。

    • 延迟记录访问:当一个条目被访问时,Caffeine 不会立即去更新一个复杂的顺序链表。它可能只是:

      1. 在条目本身记录一个 volatile 的时间戳。

      2. 或者,将这次访问事件异步地记录到缓冲区(类似于写缓冲区),再由后台线程统一处理排序逻辑。

三、解耦排序与存储

针对过期淘汰优化

  • 传统做法(Guava):维护一个按访问时间或写入时间排序的双向链表。插入、删除、移动节点都需要操作链表,意味着需要加锁。

  • Caffeine 的优化:使用 时间轮 算法来管理过期。

    • 工作原理:时间轮是一个环形的数组,每个槽代表一个时间范围(比如 1 秒)。所有在同一秒内过期的条目都被放在同一个槽对应的链表中。

    • 如何解耦

      1. 当一个条目被存入 CHM 时,会根据它的过期时间计算出它应该在时间轮的哪个槽里。

      2. 只需要将这个条目引用到那个槽的链表里即可。这个操作很快,且通常每个槽各自持有独立的锁,竞争非常少。

      3. 处理过期时,一个后台线程只需转动时间轮,取出当前槽对应的链表,批量地、异步地从 CHM 中删除这些条目。

    • 好处

      • 淘汰操作异步化:过期清理不再阻塞用户的读写请求。

      • 高性能:插入和删除过期条目的开销是常数级的 O(1),而维护一个全局排序链表的代价是 O(n)。

四、频率统计

Count-Min Sketch数据结构,一个全局的、大小固定的二维数组,用于近似统计所有key的访问频率

  • 如何解耦

    1. 每次访问时,get 方法在从 CHM 中取出值后,会异步地Count-Min Sketch 报告一次访问

    2. Count-Min Sketch 的更新是使用哈希计算定位后直接累加,冲突概率低,速度极快。

    3. 当需要淘汰时(大小已满),决策引擎会咨询 Count-Min Sketch 来比较两个条目的频率,而无需触动 CHM 的主存储结构。

  • 好处

    • 读操作与频率统计解耦get 操作的核心路径几乎不受影响。

    • 极小且固定的内存开销:无论缓存多少数据,频率统计结构的大小都不变。

Window TinyLFU算法

Caffeine将缓存区划分为两个区域:Window CacheMain Cache

  • Window Cache 是一个标准的LRU缓存,只占整个缓存内存空间大小的1%

  • Main Cache 则是一个 SLRU (Segmented LRU) ,占整个缓存内存空间大小的 99% ,是缓存的主要区域。里面进一步被划分成两个区域:Probation (试用) Cache 观察区(20%)和 Protected (保护) Cache 保护区(80%)。

  • 三个区域中的缓存都可以被访问到

  • 算法中每个缓存项由都两部分组成:数据本身、访问次数

算法流程
  • 新增缓存数据首先写入 Window Cache 区域。当 Window Cache 空间满时,LRU 算法发挥作用,最久未被访问的缓存项会被移出 Window Cache 。针对这个缓存项,分为两种情况:

    1. 如果试用缓存未满,则直接写入到试用缓存区中

    2. 试用缓存已满,则进入TinyLFU过滤器,与保护缓存区移出的缓存项比较,根据TinyLFU算法确定是否写入保护缓存区

  • 试用缓存区中的缓存项的访问频率达到一定程度后,就会晋升到保护缓存区中,如果保护缓存区也满了,根据 LRU 算法,最久未被访问的缓存项会被移出保护缓存区,降级成为缓存项,进入到TinyLFU过滤器中,与Window Cache移出的缓存项作比较,根据TinyLFU算法确定该缓存项是淘汰还是降级到试用缓存区

  • TinyLFU过滤器:汇聚了三个区域中最久未被访问的缓存项,在过滤器中竞争:

    • 如果Window Cache保护缓存区中的缓存项访问频率 > 试用缓存区中的缓存项,直接淘汰试用缓存区中的缓存项

    • 如果Window Cache保护缓存区中的缓存项访问频率 ≤ 试用缓存区中的缓存项,分两种情况:

      1. 如果Window Cache保护缓存区中的缓存项的访问频率 < 5,则淘汰Window Cache保护缓存区中的缓存项

      2. 否则这三种数据项随机淘汰一个

    • 保留的缓存数据项被写入到试用缓存区中

Count-Min Sketch

W-TinyLFU 算法使用 Count-Min Sketch 算法存储访问频率,极大地节省了空间,底层结构是一个二维数组,通过多个Hash函数计算得到缓存项在数组中的位置,当要添加数据时,在对应位置上的记录+1,判断频率时则取对应位置中的最小值作为频率次数

<think>我们正在讨论Spring Boot中同时集成Caffeine和Redis缓存的问题。根据引用内容,我们可以确认这是可行的,并且通常用于构建两级缓存(本地缓存+分布式缓存)系统。 引用[1]提到“Spring Boot 使用多级缓存 {Caffeine+Redis}”,并给出了快速入门的示例。 引用[2]解释了在分布式环境下使用Redis的发布/订阅功能来同步各个节点的缓存,解决一致性问题。 引用[3]则展示了具体的配置代码,其中通过条件注解@ConditionalOnProperty来控制本地缓存Caffeine)的开关。 因此,我们可以分步骤解释如何在Spring Boot中同时集成Caffeine和Redis: 步骤1:添加Maven依赖 需要同时添加spring-boot-starter-cache(用于支持Spring缓存抽象)、spring-boot-starter-data-redis(用于Redis)和caffeine(用于本地缓存)的依赖。 步骤2:配置缓存 在application.properties或application.yml中配置Redis和Caffeine的相关属性。 步骤3:启用缓存 在启动类上添加@EnableCaching注解。 步骤4:配置多级缓存 如引用[3]所示,可以创建配置类来定义Redis缓存Caffeine缓存,并且通过条件注解来控制Caffeine缓存的启用。 步骤5:使用缓存 在业务代码中,可以使用@Cacheable等注解来使用缓存。同时,为了支持多级缓存,可能需要自定义缓存管理器,将Caffeine作为一级缓存,Redis作为二级缓存。 引用[2]提到,为了解决分布式环境下本地缓存不一致的问题,可以使用Redis的发布/订阅功能来同步各个节点的缓存。 下面是一个简化的示例代码,展示如何配置多级缓存: ```java @Configuration @EnableCaching public class CacheConfig { // 配置Redis连接工厂 @Bean public RedisConnectionFactory redisConnectionFactory() { return new LettuceConnectionFactory(); } // 配置RedisTemplate @Bean public RedisTemplate<String, Object> redisTemplate() { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(redisConnectionFactory()); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); return template; } // 配置Redis缓存管理器 @Bean public RedisCacheManager redisCacheManager(RedisTemplate<String, Object> redisTemplate) { RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate); cacheManager.setTransactionAware(true); cacheManager.setLoadRemoteCachesOnStartup(true); cacheManager.setUsePrefix(true); return cacheManager; } // 配置Caffeine缓存管理器 @Bean public CaffeineCacheManager caffeineCacheManager() { CaffeineCacheManager cacheManager = new CaffeineCacheManager(); cacheManager.setCaffeine(Caffeine.newBuilder() .expireAfterWrite(10, TimeUnit.MINUTES) .maximumSize(1000)); return cacheManager; } // 配置多级缓存管理器(组合Caffeine和Redis) @Primary @Bean public CacheManager multiLevelCacheManager(CaffeineCacheManager caffeineCacheManager, RedisCacheManager redisCacheManager) { // 这里需要自定义一个组合缓存管理器,可以使用已有的开源实现或者自己实现 // 通常,读取时先读本地缓存,再读Redis;写入时同时写入两级缓存 // 注意:需要处理缓存同步和一致性问题 return new MultiLevelCacheManager(caffeineCacheManager, redisCacheManager); } } ``` 注意:上述代码中的MultiLevelCacheManager需要自定义实现,它负责协调两级缓存。同时,为了保持缓存一致性,当数据更新时,需要使所有节点的一级缓存失效,这可以通过Redis的发布/订阅机制实现(如引用[2]所述)。 另外,引用[3]中提到了使用条件注解来控制本地缓存的开关,我们可以这样实现: ```java @Configuration @ConditionalOnProperty(name = "cache.local.enabled", havingValue = "true") public class LocalCacheConfig { // 配置本地缓存相关的Bean } ``` 这样,通过配置文件中的`cache.local.enabled`属性,可以动态开启或关闭本地缓存。 综上所述,Spring Boot可以同时集成Caffeine和Redis,构建多级缓存系统,并通过配置灵活控制。 生成相关问题: 1. 如何自定义多级缓存管理器(MultiLevelCacheManager)? 2. 如何利用Redis的发布/订阅实现缓存同步? 3. 在Spring Boot中配置多级缓存时,需要注意哪些问题?</think>在 Spring Boot 项目中,**可以同时集成 Caffeine 和 Redis 缓存**,形成多级缓存架构(本地缓存 + 分布式缓存)。这种组合能显著提升系统性能,同时解决分布式环境下的缓存一致性问题。以下是关键实现方案: --- ### **一、实现原理** 1. **两级缓存架构** - **一级缓存(本地)**:使用 Caffeine 提供超高速访问(纳秒级响应),适合高频访问的热点数据。 - **二级缓存(分布式)**:使用 Redis 保证多节点间数据一致性,作为后备存储。 ```mermaid graph LR A[应用] --> B[Caffeine 本地缓存] B -->|缓存未命中| C[Redis 分布式缓存] C -->|缓存未命中| D[数据库] ``` 2. **缓存同步机制** - 通过 Redis 的 **发布/订阅(Pub/Sub)** 功能同步各节点的本地缓存(如引用[2]所述)。 - 当某节点修改数据时,发布消息使其他节点失效本地缓存。 --- ### **二、集成步骤** #### 1. **添加 Maven 依赖** ```xml <!-- Caffeine --> <dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> </dependency> <!-- Redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> ``` #### 2. **启用缓存(启动类)** ```java @SpringBootApplication @EnableCaching // 开启缓存抽象 public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } ``` #### 3. **配置多级缓存管理器** ```java @Configuration public class CacheConfig { @Bean public CacheManager cacheManager(RedisConnectionFactory factory) { // Caffeine 作为一级缓存(本地) CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager(); caffeineCacheManager.setCaffeine(Caffeine.newBuilder() .expireAfterWrite(10, TimeUnit.MINUTES) // 本地缓存过期时间 .maximumSize(1000) ); // Redis 作为二级缓存(分布式) RedisCacheManager redisCacheManager = RedisCacheManager.create(factory); // 组合多级缓存(优先本地,其次Redis) return new MultiLevelCacheManager(caffeineCacheManager, redisCacheManager); } } ``` #### 4. **实现缓存同步(关键)** 通过 Redis 发布/订阅同步本地缓存失效(如引用[2]): ```java @Bean public RedisMessageListenerContainer container(RedisConnectionFactory factory) { RedisMessageListenerContainer container = new RedisMessageListenerContainer(); container.setConnectionFactory(factory); container.addMessageListener(new MessageListenerAdapter(this), new ChannelTopic("cache-invalidate")); return container; } // 收到消息时失效本地缓存 public void handleMessage(String cacheKey) { caffeineCacheManager.getCache(cacheKey).clear(); } ``` #### 5. **动态开关本地缓存(如引用[3])** ```java @Configuration @ConditionalOnProperty( // 通过配置控制开关 name = "cache.local.enabled", havingValue = "true", matchIfMissing = false ) public class LocalCacheConfig { @Bean public CaffeineCacheManager caffeineCacheManager() { // 本地缓存配置 } } ``` 配置文件 `application.yml`: ```yaml cache: local: enabled: true # 动态启用/禁用 Caffeine ``` --- ### **三、使用缓存示例** ```java @Service public class UserService { @Cacheable(value = "users", key = "#id") // 自动使用多级缓存 public User getUserById(Long id) { return userRepository.findById(id); // 数据库查询 } @CacheEvict(value = "users", key = "#id") public void updateUser(User user) { // 更新数据后发布缓存失效消息 redisTemplate.convertAndSend("cache-invalidate", "users:" + user.getId()); } } ``` --- ### **四、优势与适用场景** | **优势** | **适用场景** | |-------------------------|----------------------------------| | **高性能**:Caffeine 本地缓存响应极快 | 高频读取的热点数据(如商品详情) | | **一致性**:Redis 保证多节点同步 | 分布式集群环境 | | **灵活性**:动态开关本地缓存 | 测试环境降级、流量高峰控制 | > **注意**:需权衡本地缓存带来的内存消耗,建议通过 `maximumSize` 限制缓存条目(如引用[3])。 --- ### **相关问题** 1. 如何解决 Caffeine 和 Redis 之间的数据一致性问题? 2. 在多级缓存架构中,如何设计缓存穿透和雪崩的保护机制? 3. 如何监控 Spring Boot 中 Caffeine 和 Redis 的缓存命中率? [^1]: Spring Boot 使用多级缓存 {Caffeine+Redis},快速入门示例 [^2]: 基于 Redis 发布/订阅实现分布式缓存同步 [^3]: 通过 `@ConditionalOnProperty` 动态控制本地缓存开关
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值