Redis

关注公众号 jb51net

关闭
首页 > 数据库 > Redis > Redis+Caffeine构建高性能二级缓存

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

作者:摘星编程

这篇文章主要介绍了Redis+Caffeine如何构建高性能二级缓存问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教

二级缓存架构的技术背景

1. 基础缓存架构

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

2. 架构演进动因

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

问题类型

表现特征

典型案例

超高并发读取

Redis带宽成为瓶颈

热点商品详情页访问

超低延迟要求

网络往返耗时不可忽略

金融行情数据推送

成本控制需求

高频访问导致Redis扩容

用户基础信息查询

3. 二级缓存解决方案

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

协同机制:

为什么选择本地缓存?

1. 极速访问

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

技术选型

响应时长

本地缓存

~100ns

Redis远程缓存

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

数据库查询

~10ms 甚至更长。

2. 减少网络IO

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

4. 提升系统吞吐量

5. 功能灵活

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

本地内存具备的功能

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)包的核心组件,它被广泛应用于需要线程安全哈希表的场景。

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项目中。

<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));
        
        // 使用示例
        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:版本号控制

实现原理:

// 版本号校验示例
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) // 一级缓存(堆内)
.buildAsync(key -> {
    Object value = offHeapCache.get(key); // 二级缓存(堆外)
    if(value == null) value = loadFromDB(key);
    return value;
});

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

策略类型

适用场景

配置示例

基于大小

固定数量的小对象

maximumSize(10_000)

基于权重

大小差异显著的对象

maximumWeight(1GB).weigher()

基于时间

时效性强的数据

expireAfterWrite(5min)

基于引用

非核心数据

softValues()

3. GC压力

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

缓存对象生命周期特征:

内存结构影响:

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

GC行为变化表现:

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

优势:

代价:

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(...);
    }
}

效果:

总结

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

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文