Redis+Caffeine如何構(gòu)建高性能二級緩存
二級緩存架構(gòu)的技術(shù)背景
1. 基礎(chǔ)緩存架構(gòu)
在現(xiàn)代分布式系統(tǒng)設(shè)計中,緩存是優(yōu)化服務(wù)性能的核心組件。標準實現(xiàn)方案采用遠程緩存(如Redis/Memcached)作為數(shù)據(jù)庫前置層,通過以下機制提升性能:
- 讀寫策略:遵循Cache-Aside模式,僅當緩存未命中時查詢數(shù)據(jù)庫
- 核心價值:
- 將平均響應(yīng)時間從數(shù)據(jù)庫的10-100ms級別降至1-10ms
- 降低數(shù)據(jù)庫負載50%-80%(根據(jù)命中率變化)
2. 架構(gòu)演進動因
當系統(tǒng)面臨以下場景時,純遠程緩存方案顯現(xiàn)局限性:
問題類型 | 表現(xiàn)特征 | 典型案例 |
超高并發(fā)讀取 | Redis帶寬成為瓶頸 | 熱點商品詳情頁訪問 |
超低延遲要求 | 網(wǎng)絡(luò)往返耗時不可忽略 | 金融行情數(shù)據(jù)推送 |
成本控制需求 | 高頻訪問導致Redis擴容 | 用戶基礎(chǔ)信息查詢 |
3. 二級緩存解決方案
引入本地緩存構(gòu)建兩級緩存體系:


- 一級緩存:Caffeine(高性能本地緩存)
- 二級緩存:Redis Cluster(高可用遠程緩存)
協(xié)同機制:
- 本地緩存設(shè)置短TTL(秒級)
- 遠程緩存設(shè)置長TTL(分鐘級)
- 通過PubSub實現(xiàn)跨節(jié)點失效
為什么選擇本地緩存?
1. 極速訪問
內(nèi)存級響應(yīng):本地緩存直接存儲在應(yīng)用進程的內(nèi)存中(如Java堆內(nèi)),訪問速度通常在納秒級(如Caffeine的讀寫性能可達每秒千萬次),而遠程緩存(如Redis)需要網(wǎng)絡(luò)通信,延遲在毫秒級。
技術(shù)選型 | 響應(yīng)時長 |
本地緩存 | ~100ns |
Redis遠程緩存 | ~1ms(受網(wǎng)絡(luò)影響可能更高) |
數(shù)據(jù)庫查詢 | ~10ms 甚至更長。 |
2. 減少網(wǎng)絡(luò)IO
- 避免遠程調(diào)用:每次訪問Redis都需要經(jīng)過網(wǎng)絡(luò)I/O(序列化、傳輸、反序列化),本地緩存完全繞過這一過程。
- 適用場景:高頻訪問的熱點數(shù)據(jù)(如商品詳情、用戶基礎(chǔ)信息),通過本地緩存可減少90%以上的Redis請求。
3. 降低遠程緩存和數(shù)據(jù)庫壓力
- 保護Redis:大量請求直接命中本地緩存,避免Redis成為瓶頸(尤其在高并發(fā)場景下,如秒殺、熱點查詢)。
- 減少穿透風險:本地緩存可設(shè)置短期過期時間,避免緩存失效時大量請求直接沖擊數(shù)據(jù)庫。
4. 提升系統(tǒng)吞吐量
- 減少線程阻塞:遠程緩存訪問會阻塞線程(如Redis的同步調(diào)用),本地緩存無此問題,尤其適合高并發(fā)服務(wù)。
- 案例:某電商系統(tǒng)引入Caffeine后,QPS從1萬提升到5萬,Redis負載下降60%。
5. 功能靈活
本地緩存支持豐富的特性,滿足不同業(yè)務(wù)需求:
- 淘汰策略:LRU(最近最少使用)、LFU(最不經(jīng)常使用)、FIFO等。
- 過期控制:支持基于時間(寫入后過期、訪問后過期)或容量觸發(fā)淘汰。
- 原子操作:如
get-if-absent-compute(查不到時自動加載),避免并發(fā)重復查詢。
本地內(nèi)存具備的功能
1. 基本讀寫
功能:基礎(chǔ)的鍵值存儲與原子操作。
Cache<String, String> cache = Caffeine.newBuilder().build();
// 寫入緩存
cache.put("user:1", "Alice");
// 讀取緩存(若不存在則自動計算)
String value = cache.get("user:1", key -> fetchFromDB(key));2. 緩存淘汰策略
功能:限制緩存大小并淘汰數(shù)據(jù)。
算法 | 描述 | 適用場景 | 代碼示例(Caffeine) |
LRU | 淘汰最久未訪問的數(shù)據(jù) | 熱點數(shù)據(jù)分布不均勻 | .maximumSize(100).build() |
LFU | 淘汰訪問頻率最低的數(shù)據(jù) | 長期穩(wěn)定的熱點數(shù)據(jù) | .maximumSize(100).build() (W-TinyLFU) |
FIFO | 按寫入順序淘汰 | 數(shù)據(jù)順序敏感的場景 | 需自定義實現(xiàn) |
3. 過期時間控制
功能:自動清理過期數(shù)據(jù)。
Caffeine.newBuilder() .expireAfterWrite(10, TimeUnit.MINUTES) // 寫入后10分鐘過期 .expireAfterAccess(5, TimeUnit.MINUTES) // 訪問后5分鐘過期 .build();

4. 緩存加載與刷新
功能:自動加載數(shù)據(jù)并支持后臺刷新。
AsyncLoadingCache<String, String> cache = Caffeine.newBuilder()
.refreshAfterWrite(1, TimeUnit.MINUTES) // 1分鐘后后臺刷新
.buildAsync(key -> fetchFromDB(key));
// 獲取數(shù)據(jù)(若需刷新,不會阻塞請求)
CompletableFuture<String> future = cache.get("user:1");5. 并發(fā)控制
功能:線程安全與擊穿保護。
// 自動合并并發(fā)請求(同一key僅一次加載)
LoadingCache<String, String> cache = Caffeine.newBuilder()
.build(key -> {
System.out.println("僅執(zhí)行一次: " + key);
return fetchFromDB(key);
});
// 并發(fā)測試(輸出1次日志)
IntStream.range(0, 100).parallel().forEach(
i -> cache.get("user:1")
);
6. 統(tǒng)計與監(jiān)控
功能:記錄命中率等指標。
Cache<String, String> cache = Caffeine.newBuilder()
.recordStats() // 開啟統(tǒng)計
.build();
cache.get("user:1");
CacheStats stats = cache.stats();
System.out.println("命中率: " + stats.hitRate());7. 持久化
功能:緩存數(shù)據(jù)持久化到磁盤。
// 使用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. 事件監(jiān)聽
功能:監(jiān)聽緩存變更事件。
Cache<String, String> cache = Caffeine.newBuilder()
.removalListener((key, value, cause) ->
System.out.println("移除事件: " + key + " -> " + cause))
.evictionListener((key, value, cause) ->
System.out.println("驅(qū)逐事件: " + key + " -> " + cause))
.build();本地緩存方案選型
1. ConcurrentHashMap
ConcurrentHashMap是Java集合框架中提供的線程安全哈希表實現(xiàn),首次出現(xiàn)在JDK1.5中。它采用分段鎖技術(shù)(JDK8后改為CAS+synchronized優(yōu)化),通過將數(shù)據(jù)分成多個段(segment),每個段獨立加鎖,實現(xiàn)了高并發(fā)的讀寫能力。
作為JUC(java.util.concurrent)包的核心組件,它被廣泛應(yīng)用于需要線程安全哈希表的場景。
- 原生JDK支持,零外部依賴
- 讀寫性能接近非同步的HashMap
- 完全線程安全,支持高并發(fā)
- 提供原子性復合操作(如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();
// 基礎(chǔ)操作
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內(nèi)部Java開發(fā)的標準庫,經(jīng)過大規(guī)模生產(chǎn)環(huán)境驗證,穩(wěn)定性和性能都有保障。Guava Cache廣泛應(yīng)用于各種需要本地緩存的Java項目中。
- Google背書,質(zhì)量有保證
- 豐富的緩存特性
- 良好的API設(shè)計
- 完善的文檔和社區(qū)支持
<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) // 最大條目數(shù)
.expireAfterWrite(10, TimeUnit.MINUTES) // 寫入后過期時間
.expireAfterAccess(30, TimeUnit.MINUTES) // 訪問后過期時間
.concurrencyLevel(8) // 并發(fā)級別
.recordStats() // 開啟統(tǒng)計
.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");
// 打印統(tǒng)計
System.out.println(cache.stats());
} catch (ExecutionException e) {
e.printStackTrace();
}
}
private static String loadFromDB(String key) {
// 模擬數(shù)據(jù)庫查詢
return "DB_Result_" + key;
}
}3. Caffeine
Caffeine是Guava Cache作者的新作品,發(fā)布于2015年。它專為現(xiàn)代Java應(yīng)用設(shè)計,采用Window-TinyLFU淘汰算法,相比傳統(tǒng)LRU有更高的命中率。Caffeine充分利用Java 8特性(如CompletableFuture),在性能上大幅超越Guava Cache(3-5倍提升),是目前性能最強的Java本地緩存庫。
- 超高性能
- 更高的緩存命中率
- 異步刷新機制
- 精細的內(nèi)存控制
<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");
// 打印統(tǒng)計
System.out.println(cache.stats());
}
static class Data {
int size() { return 1; }
}
private static Data loadFromDB(String key) {
// 模擬數(shù)據(jù)庫加載
return new Data();
}
}4. Encache
EEhcache是Terracotta公司開發(fā)的企業(yè)級緩存框架,始于2003年。它是JSR-107標準實現(xiàn)之一,支持從本地緩存擴展到分布式緩存。Ehcache的特色在于支持多級存儲(堆內(nèi)/堆外/磁盤),適合需要緩存持久化的企業(yè)級應(yīng)用。
最新版本Ehcache 3.x完全重構(gòu),提供了更現(xiàn)代的API設(shè)計。
- 企業(yè)級功能支持
- 多級存儲架構(gòu)
- 完善的監(jiān)控管理
- 良好的擴展性
<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) // 堆內(nèi)
.offheap(100, MemoryUnit.MB) // 堆外
.disk(1, MemoryUnit.GB, true) // 磁盤
)
.withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofMinutes(10)))
.build();
// 3. 創(chuàng)建緩存
Cache<String, String> cache = cacheManager.createCache("myCache", config);
// 4. 使用緩存
cache.put("key1", "value1");
String value = cache.get("key1");
System.out.println(value);
// 5. 關(guān)閉
cacheManager.close();
}
}方案對比
特性 | ConcurrentHashMap | Guava Cache | Caffeine | Ehcache |
基本緩存功能 | ? | ? | ? | ? |
過期策略 | ? | ? | ? | ? |
淘汰算法 | ? | LRU | W-TinyLFU | LRU/LFU |
自動加載 | ? | ? | ? | ? |
異步加載 | ? | ? | ? | ? |
持久化支持 | ? | ? | ? | ? |
多級存儲 | ? | ? | ? | ? |
命中率統(tǒng)計 | ? | 基本 | 詳細 | 詳細 |
分布式支持 | ? | ? | ? | ? |
內(nèi)存占用 | 低 | 中 | 中 | 高 |

本地緩存問題及解決
1. 數(shù)據(jù)一致性
兩級緩存與數(shù)據(jù)庫的數(shù)據(jù)要保持一致,一旦數(shù)據(jù)發(fā)生了修改,在修改數(shù)據(jù)庫的同時,本地緩存、遠程緩存應(yīng)該同步更新。
1.1. 解決方案1: 失效廣播機制
通過Redis PubSub或Rabbit MQ等消息中間件實現(xiàn)跨節(jié)點通知
- 優(yōu)點:實時性較好,能快速同步變更
- 缺點:增加了系統(tǒng)復雜度,網(wǎng)絡(luò)分區(qū)時可能失效

如果你不想在你的業(yè)務(wù)代碼發(fā)送MQ消息,還可以適用近幾年比較流行的方法:訂閱數(shù)據(jù)庫變更日志,再操作緩存。Canal 訂閱Mysql的 Binlog日志,當發(fā)生變化時向MQ發(fā)送消息,進而也實現(xiàn)數(shù)據(jù)一致性。

1.2. 解決方案2:版本號控制
實現(xiàn)原理:
- 在數(shù)據(jù)庫表中增加版本號字段(version)
- 緩存數(shù)據(jù)時同時存儲版本號
- 查詢時比較緩存版本與數(shù)據(jù)庫版本
// 版本號校驗示例
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; // 版本一致,返回緩存
}
}
// 版本不一致或緩存不存在,從數(shù)據(jù)庫加載
Product product = db.loadProduct(id);
localCache.put(id, new CacheEntry(product, product.getVersion()));
return product;
}2. 內(nèi)存管理問題
2.1. 解決方案1:分層緩存架構(gòu)
// 組合堆內(nèi)與堆外緩存
Cache<String, Object> multiLevelCache = Caffeine.newBuilder()
.maximumSize(10_000) // 一級緩存(堆內(nèi))
.buildAsync(key -> {
Object value = offHeapCache.get(key); // 二級緩存(堆外)
if(value == null) value = loadFromDB(key);
return value;
});- 使用
Window-TinyLFU算法自動識別熱點 - 對TOP 1%的熱點數(shù)據(jù)單獨配置更大容量
2.2. 解決方案2:智能淘汰策略
策略類型 | 適用場景 | 配置示例 |
基于大小 | 固定數(shù)量的小對象 | maximumSize(10_000) |
基于權(quán)重 | 大小差異顯著的對象 | maximumWeight(1GB).weigher() |
基于時間 | 時效性強的數(shù)據(jù) | expireAfterWrite(5min) |
基于引用 | 非核心數(shù)據(jù) | softValues() |
3. GC壓力
3.1. GC壓力問題的產(chǎn)生原因
緩存對象生命周期特征:
- 本地緩存通常持有大量長期存活對象(如商品信息、配置數(shù)據(jù))
- 與傳統(tǒng)短期對象(如HTTP請求作用域?qū)ο螅┎煌?,這些對象會持續(xù)晉升到老年代
- 示例:1GB的本地緩存意味著老年代常駐1GB可達對象
內(nèi)存結(jié)構(gòu)影響:
// 典型緩存數(shù)據(jù)結(jié)構(gòu)帶來的內(nèi)存開銷 ConcurrentHashMap<String, Product> cache = new ConcurrentHashMap<>(); // 實際內(nèi)存占用 = 鍵對象 + 值對象 + 哈希表Entry對象(約額外增加40%開銷)
GC行為變化表現(xiàn):
- Full GC頻率上升:從2次/天 → 15次/天(如問題描述)
- 停頓時間增長:STW時間從120ms → 可能達到秒級(取決于堆大小)
- 晉升失敗風險:當緩存大小接近老年代容量時,容易觸發(fā)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();
優(yōu)勢:
- 完全繞過JVM堆內(nèi)存管理
- 不受GC影響,內(nèi)存由操作系統(tǒng)直接管理
- 可突破JVM堆大小限制(如緩存50GB數(shù)據(jù))
代價:
- 需要手動實現(xiàn)序列化/反序列化
- 讀取時存在內(nèi)存拷貝開銷(比堆內(nèi)緩存慢約20-30%)

3.3. 方案2:分區(qū)域緩存
// 按業(yè)務(wù)劃分獨立緩存實例
public class CacheRegistry {
private static LoadingCache<String, Product> productCache = ...; // 商品專用
private static LoadingCache<Integer, UserProfile> userCache = ...; // 用戶專用
// 獨立配置各緩存參數(shù)
static {
productCache = Caffeine.newBuilder()
.maximumSize(10_000)
.build(...);
userCache = Caffeine.newBuilder()
.maximumWeight(100MB)
.weigher(...)
.build(...);
}
}效果:
- 避免單一超大緩存域?qū)е氯諫C壓力
- 可針對不同業(yè)務(wù)設(shè)置差異化淘汰策略
總結(jié)
通過以上的分析和實現(xiàn),可以通過Redis+Caffeine實現(xiàn)高性能二級緩存實現(xiàn)。
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Redis?的內(nèi)存淘汰策略和過期刪除策略的區(qū)別
這篇文章主要介紹了Redis?的內(nèi)存淘汰策略和過期刪除策略的區(qū)別,Redis?是可以對?key?設(shè)置過期時間的,因此需要有相應(yīng)的機制將已過期的鍵值對刪除,而做這個工作的就是過期鍵值刪除策略2022-07-07
一文搞懂阿里云服務(wù)器部署Redis并整合Spring?Boot
這篇文章主要介紹了一文搞懂阿里云服務(wù)器部署Redis并整合Spring?Boot,文章圍繞主題展開詳細的內(nèi)容介紹,具有一定的參考價值,需要的小伙伴可以參考一下2022-09-09

