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:分区域缓存
- 总结
二级缓存架构的技术背景
1. 基础缓存架构
在现代分布式系统设计中,缓存是优化服务性能的核心组件。标准实现方案采用远程缓存(如Redis/Memcached)作为数据库前置层,通过以下机制提升性能:
- 读写策略:遵循Cache-Aside模式,仅当缓存未命中时查询数据库
- 核心价值:
- 将平均响应时间从数据库的10-100ms级别降至1-10ms
- 降低数据库负载50%-80%(根据命中率变化)
2. 架构演进动因
当系统面临以下场景时,纯远程缓存方案显现局限性:
问题类型 | 表现特征 | 典型案例 |
超高并发读取 | Redis带宽成为瓶颈 | 热点商品详情页访问 |
超低延迟要求 | 网络往返耗时不可忽略 | 金融行情数据推送 |
成本控制需求 | 高频访问导致Redis扩容 | 用户基础信息查询 |
3. 二级缓存解决方案
引入本地缓存构建两级缓存体系:
- 一级缓存: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));
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();
4. 缓存加载与刷新
功能:自动加载数据并支持后台刷新。
AsyncLoadingCache<String, String> cache = Caffeine.newBuilder() .refreshAfterWrite(1, TimeUnit.MINUTES) // 1分钟后后台刷新 .buildAsync(key -> fetchFromDB(key)); // 获取数据(若需刷新,不会阻塞请求) CompletableFuture<String> future = cache.get("user:1");
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") );
6. 统计与监控
功能:记录命中率等指标。
Cache<String, String> cache = Caffeine.newBuilder() .recordStats() // 开启统计 .build(); cache.get("user:1"); CacheStats stats = cache.stats(); System.out.println("命中率: " + stats.hitRate());
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();
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();
本地缓存方案选型
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(); } }
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>
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; } }
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>
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)); android // 使用示例 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(); } }
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>
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(); } }
方案对比
特性 | ConcurrentHashMap | Guava Cache | Caffeine | Ehcache |
基本缓存功能 | ✓ | ✓ | ✓ | ✓ |
过期策略 | ✗ | ✓ | ✓ | ✓ |
淘汰算法 | ✗ | LRU | W-TinyLFU | LRU/LFU |
自动加载 | ✗ | ✓ | ✓ | ✓ |
异步加载 | ✗ | ✗ | ✓ | ✗ |
持久化支持 | ✗ | ✗ | ✗ | ✓ |
多级存储 | ✗ | ✗ | ✗ | ✓ |
命中率统计 | ✗ | 基本 | 详细 | 详细 |
分布式支持 | ✗ | ✗ | ✗ | ✓ |
内存占用 | 低 | 中 | 中 | 高 |
本地缓存问题及解决
1. 数据一致性
两级缓存与数据库的数据要保持一致,一旦数据发生了修改,在修改数据库的同时,本地缓存、远程缓存应该同步更新。
1.1. 解决方案1: 失效广播机制
通过Redis PubSub或Rabbit MQ等消息中间件实现跨节点通知
- 优点:实时性较好,能快速同步变更
- 缺点:增加了系统复杂度,网络分区时可能失效
如果你不想在你的业务代码发送MQ消息,还可以适用近几年比较流行的方法:订阅数据库变更日志,再操作缓存。Canal 订阅mysql的 Binlog日志,当发生变化时向MQ发送消息,进而也实现数据一致性。
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; }
2. 内存编程客栈管理问题
2.1. 解决方案1:分层缓存架构
// 组合堆内与堆外缓存 Cache<String, Object> multiLevelCache = Caffeine.newBuilder() .maximumSize(10_000) // 一级缓存(python堆内) .buildAsync(key -> { Object value = offHeapCache.get(key); // 二级缓存(堆外) if(value == null) value = loadFromDB(key); return value; });
- 使用
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%开销)
GC行为变化表现:
- Full GC频率上升:从2次/天 → 15次/天(如问题描述)
- 停顿时间增长:STW时间从120ms → 可能达到秒级(取决于堆大小)
- 晋升失败风险:当缓存js大小接近老年代容量时,容易触发Cjavascriptoncurrent 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();
优势:
- 完全绕过JVM堆内存管理
- 不受GC影响,内存由操作系统直接管理
- 可突破JVM堆大小限制(如缓存50GB数据)
代价:
- 需要手动实现序列化/反序列化
- 读取时存在内存拷贝开销(比堆内缓存慢约20-30%)
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(...); } }
效果:
- 避免单一超大缓存域导致全局GC压力
- 可针对不同业务设置差异化淘汰策略
总结
通过以上的分析和实现,可以通过Redis+Caffeine实现高性能二级缓存实现。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程客栈(www.devze.com)。
精彩评论