Redis+Caffeine构建高性能二级缓存

简介: 大家好,我是摘星。今天为大家带来的是Redis+Caffeine构建高性能二级缓存,废话不多说直接开始~


image.gif 编辑

大家好,我是摘星。今天为大家带来的是Redis+Caffeine构建高性能二级缓存,废话不多说直接开始~

目录

二级缓存架构的技术背景

1. 基础缓存架构

2. 架构演进动因

3. 二级缓存解决方案

为什么选择本地缓存?

1. 极速访问

2. 减少网络IO

3. 降低远程缓存和数据库压力

4. 提升系统吞吐量

5. 功能灵活

本地内存具备的功能

1. 基本读写

2. 缓存淘汰策略

3. 过期时间控制

4. 缓存加载与刷新

5. 并发控制

6. 统计与监控

7. 持久化

8. 事件监听

本地缓存方案选型

1. ConcurrentHashMap

2. Guava Cache

3. Caffeine

4. Encache

方案对比

本地缓存问题及解决

1. 数据一致性

1.1. 解决方案1: 失效广播机制

1.2. 解决方案2:版本号控制

2. 内存管理问题

2.1. 解决方案1:分层缓存架构

2.2. 解决方案2:智能淘汰策略

3. GC压力

3.1. GC压力问题的产生原因

3.2. 解决方案1:堆外缓存(Off-Heap Cache)

3.3. 方案2:分区域缓存

总结


image.gif 编辑

二级缓存架构的技术背景

1. 基础缓存架构

在现代分布式系统设计中,缓存是优化服务性能的核心组件。标准实现方案采用远程缓存(如Redis/Memcached)作为数据库前置层,通过以下机制提升性能:

  • 读写策略:遵循Cache-Aside模式,仅当缓存未命中时查询数据库
  • 核心价值:
  • 将平均响应时间从数据库的10-100ms级别降至1-10ms
  • 降低数据库负载50%-80%(根据命中率变化)

2. 架构演进动因

当系统面临以下场景时,纯远程缓存方案显现局限性:

问题类型

表现特征

典型案例

超高并发读取

Redis带宽成为瓶颈

热点商品详情页访问

超低延迟要求

网络往返耗时不可忽略

金融行情数据推送

成本控制需求

高频访问导致Redis扩容

用户基础信息查询

3. 二级缓存解决方案

引入本地缓存构建两级缓存体系:

image.gif 编辑

image.gif 编辑

  • 一级缓存:Caffeine(高性能本地缓存)
  • 二级缓存:Redis Cluster(高可用远程缓存)
  • 协同机制:
  • 本地缓存设置短TTL(秒级)
  • 远程缓存设置长TTL(分钟级)
  • 通过PubSub实现跨节点失效

为什么选择本地缓存?

1. 极速访问

内存级响应:本地缓存直接存储在应用进程的内存中(如Java堆内),访问速度通常在纳秒级(如Caffeine的读写性能可达每秒千万次),而远程缓存(如Redis)需要网络通信,延迟在毫秒级。

技术选型

响应时长

本地缓存

~100ns

Redis远程缓存

~1ms(受网络影响可能更高)

数据库查询

~10ms 甚至更长。

2. 减少网络IO

避免远程调用:每次访问Redis都需要经过网络I/O(序列化、传输、反序列化),本地缓存完全绕过这一过程。

适用场景:高频访问的热点数据(如商品详情、用户基础信息),通过本地缓存可减少90%以上的Redis请求。

3. 降低远程缓存和数据库压力

保护Redis:大量请求直接命中本地缓存,避免Redis成为瓶颈(尤其在高并发场景下,如秒杀、热点查询)。

减少穿透风险:本地缓存可设置短期过期时间,避免缓存失效时大量请求直接冲击数据库。

4. 提升系统吞吐量

减少线程阻塞:远程缓存访问会阻塞线程(如Redis的同步调用),本地缓存无此问题,尤其适合高并发服务。

案例:某电商系统引入Caffeine后,QPS从1万提升到5万,Redis负载下降60%。

5. 功能灵活

本地缓存支持丰富的特性,满足不同业务需求:

  • 淘汰策略:LRU(最近最少使用)、LFU(最不经常使用)、FIFO等。
  • 过期控制:支持基于时间(写入后过期、访问后过期)或容量触发淘汰。
  • 原子操作:如get-if-absent-compute(查不到时自动加载),避免并发重复查询。

本地内存具备的功能

1. 基本读写

功能:基础的键值存储与原子操作。

Cache<String, String> cache = Caffeine.newBuilder().build();
// 写入缓存
cache.put("user:1", "Alice");
// 读取缓存(若不存在则自动计算)
String value = cache.get("user:1", key -> fetchFromDB(key));

image.gif


2. 缓存淘汰策略

功能:限制缓存大小并淘汰数据。

算法

描述

适用场景

代码示例(Caffeine)

LRU

淘汰最久未访问的数据

热点数据分布不均匀

.maximumSize(100).build()

LFU

淘汰访问频率最低的数据

长期稳定的热点数据

.maximumSize(100).build()

(W-TinyLFU)

FIFO

按写入顺序淘汰

数据顺序敏感的场景

需自定义实现

3. 过期时间控制

功能:自动清理过期数据。

Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES) // 写入后10分钟过期
.expireAfterAccess(5, TimeUnit.MINUTES) // 访问后5分钟过期
.build();

image.gif

image.gif 编辑

4. 缓存加载与刷新

功能:自动加载数据并支持后台刷新。

AsyncLoadingCache<String, String> cache = Caffeine.newBuilder()
.refreshAfterWrite(1, TimeUnit.MINUTES) // 1分钟后后台刷新
.buildAsync(key -> fetchFromDB(key));
// 获取数据(若需刷新,不会阻塞请求)
CompletableFuture<String> future = cache.get("user:1");

image.gif

5. 并发控制

功能:线程安全与击穿保护。

// 自动合并并发请求(同一key仅一次加载)
LoadingCache<String, String> cache = Caffeine.newBuilder()
    .build(key -> {
        System.out.println("仅执行一次: " + key);
        return fetchFromDB(key);
    });
// 并发测试(输出1次日志)
IntStream.range(0, 100).parallel().forEach(
    i -> cache.get("user:1")
);

image.gif

image.gif 编辑

6. 统计与监控

功能:记录命中率等指标。

Cache<String, String> cache = Caffeine.newBuilder()
.recordStats() // 开启统计
.build();
cache.get("user:1");
CacheStats stats = cache.stats();
System.out.println("命中率: " + stats.hitRate());

image.gif

7. 持久化

功能:缓存数据持久化到磁盘。

// 使用Caffeine + RocksDB(需额外依赖)
Cache<String, byte[]> cache = Caffeine.newBuilder()
    .maximumSize(100)
    .writer(new CacheWriter<String, byte[]>() {
        @Override public void write(String key, byte[] value) {
            rocksDB.put(key.getBytes(), value); // 同步写入磁盘
        }
        @Override public void delete(String key, byte[] value, RemovalCause cause) {
            rocksDB.delete(key.getBytes());
        }
    })
    .build();

image.gif

8. 事件监听

功能:监听缓存变更事件。

Cache<String, String> cache = Caffeine.newBuilder()
    .removalListener((key, value, cause) -> 
        System.out.println("移除事件: " + key + " -> " + cause))
    .evictionListener((key, value, cause) -> 
        System.out.println("驱逐事件: " + key + " -> " + cause))
    .build();

image.gif

本地缓存方案选型

1. ConcurrentHashMap

ConcurrentHashMap是Java集合框架中提供的线程安全哈希表实现,首次出现在JDK1.5中。它采用分段锁技术(JDK8后改为CAS+synchronized优化),通过将数据分成多个段(segment),每个段独立加锁,实现了高并发的读写能力。作为JUC(java.util.concurrent)包的核心组件,它被广泛应用于需要线程安全哈希表的场景。

  • 原生JDK支持,零外部依赖
  • 读写性能接近非同步的HashMap
  • 完全线程安全,支持高并发
  • 提供原子性复合操作(如computeIfAbsent)
import java.util.concurrent.*;
import java.util.function.Function;
public class CHMCache<K,V> {
    private final ConcurrentHashMap<K,V> map = new ConcurrentHashMap<>(16, 0.75f, 32);
    private final ScheduledExecutorService cleaner = Executors.newSingleThreadScheduledExecutor();
    
    // 基础操作
    public void put(K key, V value) {
        map.put(key, value);
    }
    
    // 带TTL的put
    public void put(K key, V value, long ttl, TimeUnit unit) {
        map.put(key, value);
        cleaner.schedule(() -> map.remove(key), ttl, unit);
    }
    
    // 自动加载
    public V get(K key, Function<K,V> loader) {
        return map.computeIfAbsent(key, loader);
    }
    
    // 批量操作
    public void putAll(Map<? extends K, ? extends V> m) {
        map.putAll(m);
    }
    
    // 清空缓存
    public void clear() {
        map.clear();
    }
}

image.gif

2. Guava Cache

Guava Cache是Google Guava库中的缓存组件,诞生于2011年。作为ConcurrentHashMap的增强版,它添加了缓存特有的特性。Guava项目本身是Google内部Java开发的标准库,经过大规模生产环境验证,稳定性和性能都有保障。Guava Cache广泛应用于各种需要本地缓存的Java项目中。

  • Google背书,质量有保证
  • 丰富的缓存特性
  • 良好的API设计
  • 完善的文档和社区支持
<dependency>
  <groupId>com.google.guava</groupId>
  <artifactId>guava</artifactId>
  <version>31.1-jre</version>
</dependency>

image.gif

import com.google.common.cache.*;
import java.util.concurrent.TimeUnit;
public class GuavaCacheDemo {
    public static void main(String[] args) {
        LoadingCache<String, String> cache = CacheBuilder.newBuilder()
            .maximumSize(1000) // 最大条目数
            .expireAfterWrite(10, TimeUnit.MINUTES) // 写入后过期时间
            .expireAfterAccess(30, TimeUnit.MINUTES) // 访问后过期时间
            .concurrencyLevel(8) // 并发级别
            .recordStats() // 开启统计
            .removalListener(notification -> 
                System.out.println("Removed: " + notification.getKey()))
            .build(new CacheLoader<String, String>() {
                @Override
                public String load(String key) throws Exception {
                    return loadFromDB(key);
                }
            });
        
        try {
            // 自动加载
            String value = cache.get("user:1001");
            
            // 手动操作
            cache.put("config:timeout", "5000");
            cache.invalidate("user:1001");
            
            // 打印统计
            System.out.println(cache.stats());
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
    
    private static String loadFromDB(String key) {
        // 模拟数据库查询
        return "DB_Result_" + key;
    }
}

image.gif

3. Caffeine

Caffeine是Guava Cache作者的新作品,发布于2015年。它专为现代Java应用设计,采用Window-TinyLFU淘汰算法,相比传统LRU有更高的命中率。Caffeine充分利用Java 8特性(如CompletableFuture),在性能上大幅超越Guava Cache(3-5倍提升),是目前性能最强的Java本地缓存库。

  • 超高性能
  • 更高的缓存命中率
  • 异步刷新机制
  • 精细的内存控制
<dependency>
  <groupId>com.github.ben-manes.caffeine</groupId>
  <artifactId>caffeine</artifactId>
  <version>2.9.3</version>
</dependency>

image.gif

import com.github.benmanes.caffeine.cache.*;
import java.util.concurrent.TimeUnit;
public class CaffeineDemo {
    public static void main(String[] args) {
        // 同步缓存
        Cache<String, Data> cache = Caffeine.newBuilder()
            .maximumSize(10_000)
            .expireAfterWrite(5, TimeUnit.MINUTES)
            .expireAfterAccess(10, TimeUnit.MINUTES)
            .refreshAfterWrite(1, TimeUnit.MINUTES)
            .recordStats()
            .build();
        
        // 异步加载缓存
        AsyncLoadingCache<String, Data> asyncCache = Caffeine.newBuilder()
            .maximumWeight(100_000)
            .weigher((String key, Data data) -> data.size())
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .buildAsync(key -> loadFromDB(key));
        
        // 使用示例
        Data data = cache.getIfPresent("key1");
        CompletableFuture<Data> future = asyncCache.get("key1");
        
        // 打印统计
        System.out.println(cache.stats());
    }
    
    static class Data {
        int size() { return 1; }
    }
    
    private static Data loadFromDB(String key) {
        // 模拟数据库加载
        return new Data();
    }
}

image.gif

4. Encache

EEhcache是Terracotta公司开发的企业级缓存框架,始于2003年。它是JSR-107标准实现之一,支持从本地缓存扩展到分布式缓存。Ehcache的特色在于支持多级存储(堆内/堆外/磁盘),适合需要缓存持久化的企业级应用。最新版本Ehcache 3.x完全重构,提供了更现代的API设计。

  • 企业级功能支持
  • 多级存储架构
  • 完善的监控管理
  • 良好的扩展性
<dependency>
  <groupId>org.ehcache</groupId>
  <artifactId>ehcache</artifactId>
  <version>3.9.7</version>
</dependency>

image.gif

import org.ehcache.*;
import org.ehcache.config.*;
import org.ehcache.config.builders.*;
import java.time.Duration;
public class EhcacheDemo {
    public static void main(String[] args) {
        // 1. 配置缓存管理器
        CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder()
            .with(CacheManagerBuilder.persistence("/tmp/ehcache-data"))
            .build();
        cacheManager.init();
        
        // 2. 配置缓存
        CacheConfiguration<String, String> config = CacheConfigurationBuilder
            .newCacheConfigurationBuilder(
                String.class, 
                String.class,
                ResourcePoolsBuilder.newResourcePoolsBuilder()
                    .heap(1000, EntryUnit.ENTRIES)  // 堆内
                    .offheap(100, MemoryUnit.MB)    // 堆外
                    .disk(1, MemoryUnit.GB, true)   // 磁盘
            )
            .withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofMinutes(10)))
            .build();
        
        // 3. 创建缓存
        Cache<String, String> cache = cacheManager.createCache("myCache", config);
        
        // 4. 使用缓存
        cache.put("key1", "value1");
        String value = cache.get("key1");
        System.out.println(value);
        
        // 5. 关闭
        cacheManager.close();
    }
}

image.gif

方案对比

特性

ConcurrentHashMap

Guava Cache

Caffeine

Ehcache

基本缓存功能

过期策略

淘汰算法

LRU

W-TinyLFU

LRU/LFU

自动加载

异步加载

持久化支持

多级存储

命中率统计

基本

详细

详细

分布式支持

内存占用

image.gif 编辑

本地缓存问题及解决

1. 数据一致性

两级缓存与数据库的数据要保持一致,一旦数据发生了修改,在修改数据库的同时,本地缓存、远程缓存应该同步更新。

1.1. 解决方案1: 失效广播机制

通过Redis PubSub或Rabbit MQ等消息中间件实现跨节点通知

  • 优点:实时性较好,能快速同步变更
  • 缺点:增加了系统复杂度,网络分区时可能失效

image.gif 编辑

如果你不想在你的业务代码发送MQ消息,还可以适用近几年比较流行的方法:订阅数据库变更日志,再操作缓存。Canal 订阅Mysql的 Binlog日志,当发生变化时向MQ发送消息,进而也实现数据一致性。

image.gif 编辑

1.2. 解决方案2:版本号控制

  • 实现原理:
  • 在数据库表中增加版本号字段(version)
  • 缓存数据时同时存储版本号
  • 查询时比较缓存版本与数据库版本
// 版本号校验示例
public Product getProduct(long id) {
CacheEntry entry = localCache.get(id);
if (entry != null) {
    int dbVersion = db.query("SELECT version FROM products WHERE id=?", id);
    if (entry.version == dbVersion) {
        return entry.product; // 版本一致,返回缓存
    }
}
// 版本不一致或缓存不存在,从数据库加载
Product product = db.loadProduct(id);
localCache.put(id, new CacheEntry(product, product.getVersion()));
return product;
}

image.gif

2. 内存管理问题

2.1. 解决方案1:分层缓存架构

// 组合堆内与堆外缓存
Cache<String, Object> multiLevelCache = Caffeine.newBuilder()
.maximumSize(10_000) // 一级缓存(堆内)
.buildAsync(key -> {
    Object value = offHeapCache.get(key); // 二级缓存(堆外)
    if(value == null) value = loadFromDB(key);
    return value;
});

image.gif

  • 使用Window-TinyLFU算法自动识别热点
  • 对TOP 1%的热点数据单独配置更大容量

2.2. 解决方案2:智能淘汰策略

策略类型

适用场景

配置示例

基于大小

固定数量的小对象

maximumSize(10_000)

基于权重

大小差异显著的对象

maximumWeight(1GB).weigher()

基于时间

时效性强的数据

expireAfterWrite(5min)

基于引用

非核心数据

softValues()

3. GC压力

3.1. GC压力问题的产生原因

缓存对象生命周期特征:

  • 本地缓存通常持有大量长期存活对象(如商品信息、配置数据)
  • 与传统短期对象(如HTTP请求作用域对象)不同,这些对象会持续晋升到老年代
  • 示例:1GB的本地缓存意味着老年代常驻1GB可达对象

内存结构影响:

// 典型缓存数据结构带来的内存开销
ConcurrentHashMap<String, Product> cache = new ConcurrentHashMap<>();
// 实际内存占用 = 键对象 + 值对象 + 哈希表Entry对象(约额外增加40%开销)

image.gif

GC行为变化表现:

  • Full GC频率上升:从2次/天 → 15次/天(如问题描述)
  • 停顿时间增长:STW时间从120ms → 可能达到秒级(取决于堆大小)
  • 晋升失败风险:当缓存大小接近老年代容量时,容易触发Concurrent Mode Failure

3.2. 解决方案1:堆外缓存(Off-Heap Cache)

// 使用OHC(Off-Heap Cache)示例
OHCache<String, Product> ohCache = OHCacheBuilder.newBuilder()
.keySerializer(new StringSerializer())
.valueSerializer(new ProductSerializer())
.capacity(1, Unit.GB)
.build();

image.gif

优势:

  • 完全绕过JVM堆内存管理
  • 不受GC影响,内存由操作系统直接管理
  • 可突破JVM堆大小限制(如缓存50GB数据)

代价:

  • 需要手动实现序列化/反序列化
  • 读取时存在内存拷贝开销(比堆内缓存慢约20-30%)

image.gif 编辑

3.3. 方案2:分区域缓存

// 按业务划分独立缓存实例
public class CacheRegistry {
    private static LoadingCache<String, Product> productCache = ...;  // 商品专用
    private static LoadingCache<Integer, UserProfile> userCache = ...; // 用户专用
    // 独立配置各缓存参数
    static {
        productCache = Caffeine.newBuilder()
        .maximumSize(10_000)
        .build(...);
        userCache = Caffeine.newBuilder()
        .maximumWeight(100MB)
        .weigher(...)
        .build(...);
    }
}

image.gif

效果:

  • 避免单一超大缓存域导致全局GC压力
  • 可针对不同业务设置差异化淘汰策略

总结

通过以上的分析和实现,可以通过Redis+Caffeine实现高性能二级缓存实现。

目录
相关文章
|
24天前
|
存储 NoSQL Redis
阿里云高性能数据库Tair(兼容 Redis)收费价格,稳定可靠成本低
阿里云高性能云数据库Tair兼容Redis,提供Redis开源版和Tair企业版,支持多种存储介质与灵活扩展,适用于高并发场景。Tair具备亚毫秒级稳定延迟,保障业务连续性。价格方面,Redis开源版年费从72元起,Tair企业版年费从1224元起,具体费用根据配置不同有所变化。
|
12月前
|
消息中间件 缓存 NoSQL
Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。
【10月更文挑战第4天】Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。随着数据增长,有时需要将 Redis 数据导出以进行分析、备份或迁移。本文详细介绍几种导出方法:1)使用 Redis 命令与重定向;2)利用 Redis 的 RDB 和 AOF 持久化功能;3)借助第三方工具如 `redis-dump`。每种方法均附有示例代码,帮助你轻松完成数据导出任务。无论数据量大小,总有一款适合你。
203 6
|
11月前
|
缓存 NoSQL PHP
Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出
本文深入探讨了Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出。文章还介绍了Redis在页面缓存、数据缓存和会话缓存等应用场景中的使用,并强调了缓存数据一致性、过期时间设置、容量控制和安全问题的重要性。
203 5
|
11月前
|
缓存 NoSQL 数据库
运用云数据库 Tair 构建缓存为应用提速,完成任务得苹果音响、充电套装等好礼!
本活动将带大家了解云数据库 Tair(兼容 Redis),通过体验构建缓存以提速应用,完成任务,即可领取罗马仕安卓充电套装,限量1000个,先到先得。邀请好友共同参与活动,还可赢取苹果 HomePod mini、小米蓝牙耳机等精美好礼!
|
11月前
|
存储 缓存 前端开发
利用 Webpack 5 的持久化缓存来提高构建效率
【10月更文挑战第23天】利用 Webpack 5 的持久化缓存是提高构建效率的有效手段。通过合理的配置和管理,我们可以充分发挥缓存的优势,为项目的构建和开发带来更大的便利和效率提升。你可以根据项目的实际情况,结合以上步骤和方法,进一步优化和完善利用持久化缓存的策略,以达到最佳的构建效果。同时,不断探索和实践新的方法和技术,以适应不断变化的前端开发环境和需求。
|
存储 缓存 NoSQL
构建高性能Web应用:缓存的重要性及其实现
构建高性能Web应用:缓存的重要性及其实现
|
5月前
|
缓存 NoSQL 关系型数据库
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?
|
24天前
|
存储 缓存 NoSQL
Redis专题-实战篇二-商户查询缓存
本文介绍了缓存的基本概念、应用场景及实现方式,涵盖Redis缓存设计、缓存更新策略、缓存穿透问题及其解决方案。重点讲解了缓存空对象与布隆过滤器的使用,并通过代码示例演示了商铺查询的缓存优化实践。
112 1
Redis专题-实战篇二-商户查询缓存
|
24天前
|
缓存 NoSQL 关系型数据库
Redis缓存和分布式锁
Redis 是一种高性能的键值存储系统,广泛用于缓存、消息队列和内存数据库。其典型应用包括缓解关系型数据库压力,通过缓存热点数据提高查询效率,支持高并发访问。此外,Redis 还可用于实现分布式锁,解决分布式系统中的资源竞争问题。文章还探讨了缓存的更新策略、缓存穿透与雪崩的解决方案,以及 Redlock 算法等关键技术。
|
5月前
|
消息中间件 缓存 NoSQL
基于Spring Data Redis与RabbitMQ实现字符串缓存和计数功能(数据同步)
总的来说,借助Spring Data Redis和RabbitMQ,我们可以轻松实现字符串缓存和计数的功能。而关键的部分不过是一些"厨房的套路",一旦你掌握了这些套路,那么你就像厨师一样可以准备出一道道饕餮美食了。通过这种方式促进数据处理效率无疑将大大提高我们的生产力。
200 32

热门文章

最新文章