开发者

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. 二级缓存解决方案

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

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

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

    • 一级缓存: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();

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

    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")
    );

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

    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

    自动加载

    异步加载

    持久化支持

    多级存储

    命中率统计

    基本

    详细

    详细

    分布式支持

    内存占用

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

    本地缓存问题及解决

    1. 数据一致性

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

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

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

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

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

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

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

    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%)

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

    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)。

    0

    上一篇:

    下一篇:

    精彩评论

    暂无评论...
    验证码 换一张
    取 消

    最新数据库

    数据库排行榜