生產環境中如何使用Caffeine+Redis實現二級緩存(詳細分析了遇到的各種情況)

生產環境中如何使用Caffeine+Redis實現二級緩存(詳細分析了各種情況)

本篇主要講解的是實現Caffeine+Redis實現一個現成的使用流程。下一篇講解什么是Caffeine以及caffeine的使用

00背景:

使用Caffeine和Redis的二級緩存方案源自于分布式系統中對高性能高可用性低延遲數據一致性的需求。二級緩存結合了本地緩存的快速訪問能力分布式緩存的數據共享與持久化特性,解決了單極緩存的局限性,可用于高并發及分布式場景。

1.設計目標

高性能:利用 Caffeine 的低延遲和 Redis 的高吞吐量。
一致性:確保 L1 和 L2 緩存與數據源的數據一致。
高可用性:處理緩存失效、Redis 宕機等異常情況(下面會分析解決方案)。
擴展性:支持多實例部署和水平擴展。
可監控:提供命中率、延遲等指標,便于調優。

2.架構設計

架構描述

L1 緩存(Caffeine)

  • 部署在每個應用程序實例的 JVM 內存中,存儲熱點數據。
  • 特點:納秒級訪問延遲,適合高頻訪問的少量數據。
  • 限制:數據僅限當前實例,無法跨實例共享。

L2 緩存(Redis)

  • 部署為獨立的分布式緩存服務(單機、主從或集群模式)。
  • 特點:支持跨實例共享、持久化、復雜數據結構。
  • 限制:微秒到毫秒級延遲,受網絡影響。

數據源

  • 數據庫 MySQL)
  • 當 L1 和 L2 緩存均未命中時,從數據源加載數據。

工作流程

  1. 讀取數據:

    客戶端請求數據,應用程序首先查詢 L1 緩存(Caffeine)。

    若 L1 未命中(cache miss),查詢 L2 緩存(Redis)。

    若 L2 也未命中,從數據源(如數據庫)加載數據。

    將數據寫入 L2(Redis),并回填到 L1(Caffeine)。

  2. 更新數據

    數據更新時,先更新數據庫
    然后通過Redis的發布/訂閱機制通知所有應用實例,每個實例刪除或更新本地L1緩存。由于發布/訂閱不會持久化消息,可以使用消息隊列替換Redis中的發布/訂閱

  3. 緩存同步

    使用Redis的Pub/Sub或其他消息隊列廣播失效消息
    確保L1和L2緩存與數據源保持一致

3.實現代碼

首先加入依賴

<dependency><groupId>com.github.ben-manes.caffeine</groupId><artifactId>caffeine</artifactId><version>2.9.3</version>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

代碼如下:

package com.example;import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.github.benmanes.caffeine.cache.stats.CacheStats;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.api.sync.RedisCommands;
import io.lettuce.core.pubsub.RedisPubSubAdapter;
import io.lettuce.core.pubsub.StatefulRedisPubSubConnection;
import io.lettuce.core.pubsub.api.async.RedisPubSubAsyncCommands;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import javax.annotation.PostConstruct;
import java.time.Duration;
@Service
public class TwoLevelCacheService1 {// Caffeine 緩存(L1)private final LoadingCache<String, User> caffeineCache;// Redis 連接(L2)private final StatefulRedisConnection<String, String> redisConnection;// Redis Pub/Sub 連接(用于緩存失效通知)private final StatefulRedisPubSubConnection<String, String> redisPubSubConnection;// 數據源(模擬數據庫)private final UserRepository userRepository;// Redis 緩存前綴private static final String CACHE_PREFIX = "user:";// Redis Pub/Sub 頻道private static final String INVALIDATE_CHANNEL = "user:invalidate";@Autowiredpublic TwoLevelCacheService1(StatefulRedisConnection<String, String> redisConnection,StatefulRedisPubSubConnection<String, String> redisPubSubConnection,UserRepository userRepository) {this.redisConnection = redisConnection;this.redisPubSubConnection = redisPubSubConnection;this.userRepository = userRepository;// 配置 Caffeine 緩存this.caffeineCache = Caffeine.newBuilder().maximumSize(1000) // 最大緩存 1000 個用戶.expireAfterWrite(Duration.ofMinutes(10)) // 寫入后 10 分鐘過期.recordStats() // 開啟統計.build(this::loadFromRedisOrDb); // 加載邏輯}// 初始化 Pub/Sub 監聽@PostConstructpublic void initPubSub() {//添加一個監聽器,處理接收的消息redisPubSubConnection.addListener(new RedisPubSubAdapter<String,String>() {@Overridepublic void message(String channel, String message) {if (INVALIDATE_CHANNEL.equals(channel)) {caffeineCache.invalidate(message); // 失效 L1 緩存}}});//獲取Redis Pub/Sub連接的異步命令接口,并訂閱指定的頻道RedisPubSubAsyncCommands<String, String> async = redisPubSubConnection.async();async.subscribe(INVALIDATE_CHANNEL);}// 獲取用戶(先查 L1,再查 L2,最后查數據庫)public User getUser(String userId) {return caffeineCache.get(userId);}// 更新用戶并失效緩存public void updateUser(User user) {// 更新數據庫userRepository.save(user);// 失效 L2 緩存RedisCommands<String, String> commands = redisConnection.sync();commands.del(CACHE_PREFIX + user.getId());// 廣播失效消息,通知所有實例失效 L1 緩存commands.publish(INVALIDATE_CHANNEL, user.getId());}// 從 Redis 或數據庫加載數據private User loadFromRedisOrDb(String userId) {// 查 Redis (L2)RedisCommands<String, String> commands = redisConnection.sync();String cachedUser = commands.get(CACHE_PREFIX + userId);if (cachedUser != null) {return deserializeUser(cachedUser); // 反序列化}// Redis 未命中,查數據庫,查詢到的數據存到Redis,并且隱式的存入到caffeineCache中User user = userRepository.findById(userId);if (user != null) {// 回填 Redis,設置 1 小時過期commands.setex(CACHE_PREFIX + userId, 3600, serializeUser(user));}return user;}// 序列化用戶對象(示例使用 JSON)private String serializeUser(User user) {return "{\"id\":\"" + user.getId() + "\",\"name\":\"" + user.getName() + "\"}";}// 反序列化用戶對象private User deserializeUser(String data) {// 簡單解析 JSON,生產環境建議使用 Jackson 或 GsonString[] parts = data.replaceAll("[{}\"]", "").split(",");String id = parts[0].split(":")[1];String name = parts[1].split(":")[1];return new User(id, name);}// 獲取緩存統計信息public CacheStats getCacheStats() {return caffeineCache.stats();}
}

4.配置代碼

spring:redis:host: localhostport: 6379lettuce:pool:max-active: 100max-idle: 10min-idle: 5timeout: 2000

5.異常處理

  1. Redis宕機的話直接回退到數據庫查詢
    在loadFromRedisOrDb方法中捕獲Redis異常:

    try {String cachedUser = commands.get(CACHE_PREFIX + userId);if (cachedUser != null) {return deserializeUser(cachedUser);}
    } catch (Exception e) {// 記錄日志,降級到數據庫邏輯log.error("Redis error, fallback to DB", e);
    }
    
  2. Caffeine加載失敗
    若加載邏輯拋出異常,返回默認值或拋出自定義異常

    caffeineCache = Caffeine.newBuilder().build(key -> {try {return loadFromRedisOrDb(key);} catch (Exception e) {throw new CacheException("Failed to load key: " + key, e);}});
    

6.監控與調優

  1. caffeine監控

    通過caffeineCache.stats()獲取命中率、驅逐率、加載時間

    集成Prometheus或者Micrometer,暴露指標:

7.補充

  1. 不知道大家會有這樣的疑問沒,因為Caffeine未命中后會查Redis,Redis命中后會將數據寫入Caffeine中,Redis沒有命中就會查數據庫,然后將數據分別寫入Redis和Caffeine中,那么Caffeine和Redis中的數據不就是高度重合了嗎?
    答:其實并不會高度重合,因為在Caffeine中會設置容量,比如我們這里設置的1000條,并且Caffeine達到1000后會通過LRU(最近最少使用)驅逐策略去刪除舊數據。因為根據LRU驅逐策略留下的數據都是高訪問量的數據。
  2. 對于代碼中的 buid函數
this.caffeineCache = Caffeine.newBuilder().maximumSize(1000) // 最大緩存 1000 個用戶.expireAfterWrite(Duration.ofMinutes(10)) // 寫入后 10 分鐘過期.recordStats() // 開啟統計.build(this::loadFromRedisOrDb); // 加載邏輯

這里的build需要接收一個CacheLoader類型的參數,CacheLoader是接口(函數式接口)

  @NonNullpublic <K1 extends K, V1 extends V> LoadingCache<K1, V1> build(@NonNull CacheLoader<? super K1, V1> loader) {...........}

CacheLoader使用了@FunctionalInterface注解,說明是函數時接口,其中只有一個load抽象方法

@FunctionalInterface
@SuppressWarnings({"PMD.SignatureDeclareThrowsException", "FunctionalInterfaceMethodChanged"})
public interface CacheLoader<K, V> extends AsyncCacheLoader<K, V> {@NullableV load(@NonNull K key) throws Exception;
}

那么buid的代碼就可以進行優化,Java編譯器根據caffeineCache的類型(LoadingCache<String,User>)推斷出key=String,value=User,發現與loadFromRedisOrDb方法一致,因此可以使用this::loadFromRedisOrDb作為參數

.build(new CacheLoader<String, User>() {@Overridepublic @Nullable User load(@NonNull String key) throws Exception {return loadFromRedisOrDb(key);}}); // 加載邏輯
//----->>>
.build((userId)->loadFromRedisOrDb(userId)); // 加載邏輯
//------>>>
.build(this::loadFromRedisOrDb); // 加載邏輯

Java 的函數式接口機制允許將方法引用直接賦值給接口類型,只要簽名匹配。

this::loadFromRedisOrDb 的簽名 User (String) 滿足 CacheLoader<String, User> 的要求,編譯器自動適配。

8.使用RedisTemplate實現

主要代碼:

package com.example;import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.github.benmanes.caffeine.cache.stats.CacheStats;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.api.sync.RedisCommands;
import io.lettuce.core.pubsub.RedisPubSubAdapter;
import io.lettuce.core.pubsub.StatefulRedisPubSubConnection;
import io.lettuce.core.pubsub.api.async.RedisPubSubAsyncCommands;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;import javax.annotation.PostConstruct;
import java.time.Duration;@Service
public class TwoLevelCacheService_RedisTemplate {// Caffeine 緩存(L1)private final LoadingCache<String, User> caffeineCache;//RedisTemplateprivate final RedisTemplate<String, String> redisTemplate;// 數據源(模擬數據庫)private final UserRepository userRepository;// Redis 緩存前綴private static final String CACHE_PREFIX = "user:";// Redis Pub/Sub 頻道private static final String INVALIDATE_CHANNEL = "user:invalidate";@Autowiredpublic TwoLevelCacheService_RedisTemplate(RedisTemplate<String, String> redisTemplate,UserRepository userRepository) {this.redisTemplate = redisTemplate;this.userRepository = userRepository;// 配置 Caffeine 緩存this.caffeineCache = Caffeine.newBuilder().maximumSize(1000) // 最大緩存 1000 個用戶.expireAfterWrite(Duration.ofMinutes(10)) // 寫入后 10 分鐘過期.recordStats() // 開啟統計.build(this::loadFromRedisOrDb); // 加載邏輯}// 獲取用戶(先查 L1,再查 L2,最后查數據庫)public User getUser(String userId) {return caffeineCache.get(userId);}// 更新用戶并失效緩存public void updateUser(User user) {// 更新數據庫userRepository.save(user);// 失效 L2 緩存redisTemplate.delete(CACHE_PREFIX + user.getId());// 廣播失效消息,通知所有實例失效 L1 緩存redisTemplate.convertAndSend(INVALIDATE_CHANNEL, user.getId());}// 從 Redis 或數據庫加載數據private User loadFromRedisOrDb(String userId) {// 查 Redis (L2)String cachedUser = redisTemplate.opsForValue().get(CACHE_PREFIX + userId);if (cachedUser != null) {return deserializeUser(cachedUser); // 反序列化}// Redis 未命中,查數據庫,查詢到的數據存到Redis,并且隱式的存入到caffeineCache中User user = userRepository.findById(userId);if (user != null) {// 回填 Redis,設置 1 小時過期redisTemplate.opsForValue().set(CACHE_PREFIX+user.getId(),serializeUser(user),Duration.ofHours(1));}return user;}// 序列化用戶對象(示例使用 JSON)private String serializeUser(User user) {return "{\"id\":\"" + user.getId() + "\",\"name\":\"" + user.getName() + "\"}";}// 反序列化用戶對象private User deserializeUser(String data) {// 簡單解析 JSON,生產環境建議使用 Jackson 或 GsonString[] parts = data.replaceAll("[{}\"]", "").split(",");String id = parts[0].split(":")[1];String name = parts[1].split(":")[1];return new User(id, name);}// 獲取緩存統計信息public CacheStats getCacheStats() {return caffeineCache.stats();}
}

配置類 (redisTempalte和訂閱channel)

@Configuration
public class RedisConfig {/*** 配置Redis模板*/@Beanpublic RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {RedisTemplate<String, String> template = new RedisTemplate<>();template.setConnectionFactory(factory);// 設置序列化器,確保鍵值是字符串template.setKeySerializer(new StringRedisSerializer());template.setValueSerializer(new StringRedisSerializer());template.afterPropertiesSet();return template;}/*** 配置監聽容器*/@Beanpublic RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory,LoadingCache<String, User> caffeineCache) {RedisMessageListenerContainer container = new RedisMessageListenerContainer();container.setConnectionFactory(connectionFactory);container.addMessageListener(new CacheInvalidationListener(caffeineCache),new ChannelTopic("user:invalidate"));return container;}
}

配置消息監聽處理邏輯

public class CacheInvalidationListener implements MessageListener {private final LoadingCache<String, User> caffeineCache;private static final String INVALIDATE_CHANNEL = "user:invalidate";public CacheInvalidationListener(LoadingCache<String, User> caffeineCache) {this.caffeineCache = caffeineCache;}@Overridepublic void onMessage(Message message, byte[] pattern) {String channel = message.getChannel().toString();if (INVALIDATE_CHANNEL.equals(channel)){String userId = message.getBody().toString();caffeineCache.invalidate(userId);}}
}

需要補充的地方請大家在下面留言一起討論

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/79524.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/79524.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/79524.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

RT-Thread開發文檔合集

瑞薩VisionBoard開發實踐指南 RT-Thread 文檔中心 RT-Thread-【RA8D1-Vision Board】 RA8D1 Vision Board上的USB實踐RT-Thread問答社區 - RT-Thread 【開發板】環境篇&#xff1a;05燒錄工具介紹_嗶哩嗶哩_bilibili 【RA8D1-Vision Board】基于OpenMV 實現圖像分類_嗶哩嗶哩_…

甘果桌面tv版下載-甘果桌面安卓電視版使用教程

甘果桌面 TV 版是一款備受關注的應用&#xff0c;它可以讓安卓電視的界面更加個性化、操作更加便捷。接下來&#xff0c;我們就詳細了解一下甘果桌面 TV 版的下載方法以及安卓電視版的使用教程。 甘果桌面 TV 版下載 打開你的安卓電視&#xff0c;找到并進入電視自帶的應用商店…

RAII資源管理理解

基礎介紹 RAII (Resource Acquisition Is Initialization) 是一種 C 編程范式&#xff0c;這不是一個語法特性&#xff0c;而是一種處理方式。RAII的思想&#xff1a; 資源獲取與對象初始化同時發生資源釋放與對象銷毀同時發生通過對象的生命周期來管理資源&#xff0c;確保資…

解鎖元生代:ComfyUI工作流與云原生后端的深度融合

目錄 藍耘元生代&#xff1a;智算新勢力崛起? ComfyUI 工作流創建詳解? ComfyUI 初印象? 藍耘平臺上搭建 ComfyUI 工作流? 構建基礎工作流實操? 代碼示例與原理剖析? 云原生后端技術全景 云原生后端概念解析? 核心技術深度解讀? 藍耘元生代中兩者的緊密聯系?…

實戰篇|多總線網關搭建與量產驗證(5000 字深度指南)

引言 1. 環境準備與硬件選型 1.1 項目需求分析 1.2 SoC 與開發板選型 1.3 物理接口與 PCB 設計 1.4 電源與供電保護 2. 軟件架構與協議棧移植 2.1 分層架構詳解 2.2 協議棧移植步驟 2.3 高可用驅動設計 2.4 映射邏輯與 API 定義 3. 開發流程與實踐 3.1 敏捷迭代與里程碑 3.2 核…

Kafka安全認證技術:SASL/SCRAM-ACL方案詳解

#作者 &#xff1a;張桐瑞 文章目錄 1Kafka安全認證技術介紹2基礎設置3 配置SASL/SCRAM認證3.1編寫server.properties配置3.2編寫kafka.conf密碼文件3.3編寫user.properties配置文件3.4編寫kafka-run-class.sh腳本文件3.5Zk中增加kafka用戶3.6啟動kafka進程 1Kafka安全認證技術…

TCP/IP和UDP協議的發展歷程

TCP/IP和UDP協議的發展歷程 引言 互聯網的發展史是人類技術創新的輝煌篇章&#xff0c;而在這一發展過程中&#xff0c;通信協議發揮了奠基性的作用。TCP/IP&#xff08;傳輸控制協議/互聯網協議&#xff09;和UDP&#xff08;用戶數據報協議&#xff09;作為互聯網通信的基礎…

PhotoShop學習10

1.畫板功能的使用 使用畫板功能可以輕松針對不同的設備和屏幕尺寸設計網頁和 APP。畫板是一種容器&#xff0c;類似于特殊圖層組。畫板中的圖層在圖層面板中&#xff0c;按畫板進行分組。 使用畫板&#xff0c;一個文檔中可以有多個設計版面&#xff0c;這樣可以在畫板之間輕…

X-AnyLabeling開源程序借助 Segment Anything 和其他出色模型的 AI 支持輕松進行數據標記。

一、軟件介紹 文末提供源碼和程序下載學習 使用 X-AnyLabeling開源程序可以 導入、管理和保存數據。用戶可以通過多種方式導入圖像和視頻文件&#xff0c;包括快捷方式或菜單選項。此外&#xff0c;它還涵蓋數據刪除、圖像切換以及標簽和圖像數據的保存&#xff0c;以確保高效…

【深度解析】PlatformIO多環境配置實踐:ESP32/ESP32-S3/ESP32-C3適配指南

一、前言&#xff1a;為什么需要多環境配置&#xff1f; 在物聯網開發中&#xff0c;我們經常需要適配不同型號的硬件平臺&#xff08;如ESP32系列&#xff09;,并且github上多數關于ESP32的都適配了多種開發板。傳統開發方式需要為每個平臺維護獨立項目&#xff0c;而Platfor…

React 列表渲染基礎示例

React 中最常見的一個需求就是「把一組數據渲染成一組 DOM 元素」&#xff0c;比如一個列表。下面是我寫的一個最小示例&#xff0c;目的是搞清楚它到底是怎么工作的。 示例代碼 // 定義一個靜態數組&#xff0c;模擬后續要渲染的數據源 // 每個對象代表一個前端框架&#xf…

NHANES指標推薦:CMI

文章題目&#xff1a;Association between cardiometabolic index and biological ageing among adults: a population-based study DOI&#xff1a;10.1186/s12889-025-22053-3 中文標題&#xff1a;成年人心臟代謝指數與生物衰老之間的關系&#xff1a;一項基于人群的研究 發…

QT調用ffmpeg庫實現視頻錄制

可以通過QProcess調用ffmpeg命令行,也可以直接調用ffmpeg庫,方便。 調用庫 安裝ffmpeg ffmpeg -version 沒裝就裝 sudo apt-get update sudo apt-get install ffmpeg sudo apt-get install ffmpeg libavdevice-dev .pro引入庫路徑,引入庫 LIBS += -L/usr/lib/aarch64-l…

消息中間件——RocketMQ(二)

前言&#xff1a;此篇文章系本人學習過程中記錄下來的筆記&#xff0c;里面難免會有不少欠缺的地方&#xff0c;誠心期待大家多多給予指教。 RocketMQ&#xff08;一&#xff09; 接上期內容&#xff1a;上期完成了RocketMQ單機部署知識。下面學習RocketMQ集群相關知識&#xf…

pyqt環境配置

文章目錄 1 概述2 PyQt6和PySide6區別3 環境配置4 配置PySide65 配置PyQt66 配置外部工具7 添加模板8 使用pyside6-project構建工程9 常見錯誤10 相關地址 更多精彩內容&#x1f449;內容導航 &#x1f448;&#x1f449;Qt開發 &#x1f448;&#x1f449;python開發 &#x1…

金融數據庫轉型實戰讀后感

榮幸收到老友太保科技有限公司數智研究院首席專家林春的簽名贈書。 這是國內第一本關于OceanBase數據庫實際替換過程總結的的實戰書。打個比方可以說是從戰場上下來分享戰斗經驗。讀后感受頗深。我在這里講講我的感受。 第三章中提到的應用改造如何降本。應用改造是國產化替換…

旅游資源網站登錄(jsp+ssm+mysql5.x)

旅游資源網站登錄(jspssmmysql5.x) 旅游資源網站是一個為旅游愛好者提供全面服務的平臺。網站登錄界面簡潔明了&#xff0c;用戶可以選擇以管理員或普通用戶身份登錄。成功登錄后&#xff0c;用戶可以訪問個人中心&#xff0c;進行修改密碼和個人信息管理。用戶管理模塊允許管…

STM32 HAL庫之WDG示例代碼

獨立看門狗&#xff08;IWDG&#xff09; 在規定時間內按按鍵喂狗并將LED關閉&#xff0c;若產生看門狗復位則LED打開 初始化獨立看門狗&#xff0c;在main.c中的 MX_IWDG_Init();&#xff0c;也就是iwdg.c中的初始化代碼 void MX_IWDG_Init(void) {/* USER CODE BEGIN IWDG…

【第47節】windows程序的其他反調試手段下篇

目錄 一、利用Hardware Breakpoints Detection 二、PatchingDetection - CodeChecksumCalculation 補丁檢測&#xff0c;代碼檢驗和 三、block input 封鎖鍵盤、鼠標輸入 四、使用EnableWindow 禁用窗口 五、利用ThreadHideFromDebugger 六、使用Disabling Breakpoints 禁…

【筆記ing】AI大模型-03深度學習基礎理論

神經網絡&#xff1a;A neural network is a network or circuit of neurons,or in a modern sense,an artificial neural network,composed of artificial neurons or nodes.神經網絡是神經元的網絡或回路&#xff0c;或者在現在意義上來說&#xff0c;是一個由人工神經元或節…