【 Redis | 完結篇 緩存優化 】

前言:本節包含常見redis緩存問題,包含緩存一致性問題,緩存雪崩,緩存穿透,緩存擊穿問題及其解決方案

1. 緩存一致性

我們先看下目前企業用的最多的緩存模型。緩存的通用模型有三種:

緩存模型解釋
Cache Aside

由緩存調用者自己維護數據庫與緩存的一致性

查詢時:命中則直接返回,未命中則查詢數據庫并寫入緩存

更新時:更新數據庫并刪除緩存,查詢時自然會更新緩存

Read/Write Through

數據庫自己維護一份緩存,底層實現對調用者透明

查詢時:命中則直接返回,未命中則查詢數據庫并寫入緩存

判斷緩存是否存在,不存在直接更新數據庫。存在則更新緩存,同步更新數據庫

Write Behind Caching讀寫操作都直接操作緩存,由線程異步的將緩存數據同步到數據庫

目前項目中中使用最多的是Cache Aside模式,因為實現起來非常簡單。

在Cache Aside模式中,以下有兩點需要注意:

1.在對數據庫進行增刪改操作時,需要加入清理緩存邏輯

在數據庫進行增刪改等操作時,數據庫中數據會發生變化,但是Redis中緩存的數據未發生變化從而導致數據庫和Redis中的緩存的數據不一致,故而在對數據庫進行操作時,需要加入清理緩存邏輯來清理Redis中對應的未同步緩存數據

2.先更新數據庫再刪除緩存的方案

異常情況說明:

  • 線程1查詢緩存未命中,于是去查詢數據庫,查詢到舊數據

  • 線程1將數據寫入緩存之前,線程2來了,更新數據庫,刪除緩存

  • 線程1執行寫入緩存的操作,寫入舊數據

???????可以發現,異常狀態發生的概率極為苛刻,線程1必須是查詢數據庫已經完成,但是緩存尚未寫入之前。線程2要完成更新數據庫同時刪除緩存的兩個操作。要知道線程1執行寫緩存的速度在毫秒之間,速度非常快,在這么短的時間要完成數據庫和緩存的操作,概率非常之低。

面試題如何保證緩存的雙寫一致性

:緩存的雙寫一致性很難保證強一致,只能盡可能降低不一致的概率,確保最終一致。我們項目中采用的是Cache Aside模式。簡單來說,就是在更新數據庫之后刪除緩存;在查詢時先查詢緩存,如果未命中則查詢數據庫并寫入緩存。同時我們會給緩存設置過期時間作為兜底方案,如果真的出現了不一致的情況,也可以通過緩存過期來保證最終一致。

追問:為什么不采用延遲雙刪機制?

:延遲雙刪的第一次刪除并沒有實際意義,第二次采用延遲刪除主要是解決數據庫主從同步的延遲問題,我認為這是數據庫主從的一致性問題,與緩存同步無關。既然主節點數據已經更新,Redis的緩存理應更新。而且延遲雙刪會增加緩存業務復雜度,也沒能完全避免緩存一致性問題,投入回報比太低。


2. 緩存穿透

什么是緩存穿透呢?

我們知道,當請求查詢緩存未命中時,需要查詢數據庫以加載緩存。但是大家思考一下這樣的場景:

如果我訪問一個數據庫中也不存在的數據。會出現什么現象?

????????由于數據庫中不存在該數據,那么緩存中肯定也不存在。因此不管請求該數據多少次,緩存永遠不可能建立,請求永遠會直達數據庫。

????????假如有不懷好意的人,開啟很多線程頻繁的訪問一個數據庫中也不存在的數據。由于緩存不可能生效,那么所有的請求都訪問數據庫,可能就會導致數據庫因過高的壓力而宕機。


2.1 緩存空值

簡單來說,就是當我們發現請求的數據即不存在與緩存,也不存在與數據庫時,將空值緩存到Redis,避免頻繁查詢數據庫。實現思路如下:

核心思路如下:

在原來的邏輯中,我們如果發現這個數據在mysql中不存在,直接就返回404了,這樣是會存在緩存穿透問題的

現在的邏輯中:如果這個數據不存在,我們不會返回404 ,還是會把這個數據寫入到Redis中,并且將value設置為空,歐當再次發起查詢時,我們如果發現命中之后,判斷這個value是否是null,如果是null,則是之前寫入的數據,證明是緩存穿透數據,如果不是,則直接返回數據。


2.2 布隆過濾器

布隆過濾是一種數據統計的算法,用于檢索一個元素是否存在一個集合中。

一般我們判斷集合中是否存在元素,都會先把元素保存到類似于樹、哈希表等數據結構中,然后利用這些結構查詢效率高的特點來快速匹配判斷。但是隨著元素數量越來越多,這種模式對內存的占用也越來越大,檢索的速度也會越來越慢。而布隆過濾的內存占用小,查詢效率卻很高。

此時,我們要判斷元素是否存在,只需要再次基于Khash函數做運算, 得到K個角標,判斷每個角標的位置是不是1:

  • 只要全是1,就證明元素存在

  • 任意位置為0,就證明元素一定不存在

假如某個元素本身并不存在,也沒添加到布隆過濾器過。但是由于存在hash碰撞的可能性,這就會出現這個元素計算出的角標已經被其它元素置為1的情況。那么這個元素也會被誤判為已經存在。

因此,布隆過濾器的判斷存在誤差:

  • 當布隆過濾器認為元素不存在時,它肯定不存在

  • 當布隆過濾器認為元素存在時,它可能存在,也可能不存在

我們可以把數據庫中的數據利用布隆過濾器標記出來,當用戶請求緩存未命中時,先基于布隆過濾器判斷。如果不存在則直接拒絕請求,存在則去查詢數據庫。盡管布隆過濾存在誤差,但一般都在0.01%左右,可以大大減少數據庫壓力。

面試題如何解決緩存穿透問題

:緩存穿透也可以說是穿透攻擊,具體來說是因為請求訪問到了數據庫不存在的值,這樣緩存無法命中,必然訪問數據庫。如果高并發的訪問這樣的接口,會給數據庫帶來巨大壓力。

我們項目中都是基于布隆過濾器來解決緩存穿透問題的,當緩存未命中時基于布隆過濾器判斷數據是否存在。如果不存在則不去訪問數據庫。

當然,也可以使用緩存空值的方式解決,不過這種方案比較浪費內存。


3. 緩存雪崩

面試題如何解決緩存雪崩問題

:緩存雪崩的常見原因有兩個,第一是因為大量key同時過期。針對問這個題我們可以可以給緩存key設置不同的TTL值,避免key同時過期。

第二個原因是Redis宕機導致緩存不可用。針對這個問題我們可以利用集群提高Redis的可用性。也可以添加多級緩存,當Redis宕機時還有本地緩存可用。


4.緩存擊穿

4.1 互斥鎖

核心思路:相較于原來從緩存中查詢不到數據后直接查詢數據庫而言,現在的方案是 進行查詢之后,如果從緩存沒有查詢到數據,則進行互斥鎖的獲取,獲取互斥鎖后,判斷是否獲得到了鎖,如果沒有獲得到,則休眠,過一會再進行嘗試,直到獲取到鎖為止,才能進行查詢。

如果獲取到了鎖的線程,再去進行查詢,查詢后將數據寫入redis,再釋放鎖,返回數據,利用互斥鎖就能保證只有一個線程去執行操作數據庫的邏輯,防止緩存擊穿。

操作鎖的代碼:

核心思路就是利用redis的setnx方法來表示獲取鎖,該方法含義是redis中如果沒有這個key,則插入成功,返回1,在stringRedisTemplate中返回true, 如果有這個key則插入失敗,則返回0,在stringRedisTemplate返回false,我們可以通過true,或者是false,來表示是否有線程成功插入key,成功插入的key的線程我們認為他就是獲得到鎖的線程。


private boolean tryLock(String key) {Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);return BooleanUtil.isTrue(flag);
}private void unlock(String key) {stringRedisTemplate.delete(key);
}

操作代碼:

 public Shop queryWithMutex(Long id)  {String key = CACHE_SHOP_KEY + id;// 1、從redis中查詢商鋪緩存String shopJson = stringRedisTemplate.opsForValue().get("key");// 2、判斷是否存在if (StrUtil.isNotBlank(shopJson)) {// 存在,直接返回return JSONUtil.toBean(shopJson, Shop.class);}//判斷命中的值是否是空值if (shopJson != null) {//返回一個錯誤信息return null;}// 4.實現緩存重構//4.1 獲取互斥鎖String lockKey = "lock:shop:" + id;Shop shop = null;try {boolean isLock = tryLock(lockKey);// 4.2 判斷否獲取成功if(!isLock){//4.3 失敗,則休眠重試Thread.sleep(50);return queryWithMutex(id);}//4.4 成功,根據id查詢數據庫shop = getById(id);// 5.不存在,返回錯誤if(shop == null){//將空值寫入redisstringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);//返回錯誤信息return null;}//6.寫入redisstringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_NULL_TTL,TimeUnit.MINUTES);}catch (Exception e){throw new RuntimeException(e);}finally {//7.釋放互斥鎖unlock(lockKey);}return shop;}

4.2 邏輯過期

需求:修改根據id查詢商鋪的業務,基于邏輯過期方式來解決緩存擊穿問題

思路分析:當用戶開始查詢redis時,判斷是否命中,如果沒有命中則直接返回空數據,不查詢數據庫,而一旦命中后,將value取出,判斷value中的過期時間是否滿足,如果沒有過期,則直接返回redis中的數據,如果過期,則在開啟獨立線程后直接返回之前的數據,獨立線程去重構數據,重構完成后釋放互斥鎖。

private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
public Shop queryWithLogicalExpire( Long id ) {String key = CACHE_SHOP_KEY + id;// 1.從redis查詢商鋪緩存String json = stringRedisTemplate.opsForValue().get(key);// 2.判斷是否存在if (StrUtil.isBlank(json)) {// 3.存在,直接返回return null;}// 4.命中,需要先把json反序列化為對象RedisData redisData = JSONUtil.toBean(json, RedisData.class);Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);LocalDateTime expireTime = redisData.getExpireTime();// 5.判斷是否過期if(expireTime.isAfter(LocalDateTime.now())) {// 5.1.未過期,直接返回店鋪信息return shop;}// 5.2.已過期,需要緩存重建// 6.緩存重建// 6.1.獲取互斥鎖String lockKey = LOCK_SHOP_KEY + id;boolean isLock = tryLock(lockKey);// 6.2.判斷是否獲取鎖成功if (isLock){CACHE_REBUILD_EXECUTOR.submit( ()->{try{//重建緩存this.saveShop2Redis(id,20L);}catch (Exception e){throw new RuntimeException(e);}finally {unlock(lockKey);}});}// 6.4.返回過期的商鋪信息return shop;
}

面試題如何解決緩存擊穿問題

:緩存擊穿往往是由熱點Key引起的,當熱點Key過期時,大量請求涌入同時查詢,發現緩存未命中都會去訪問數據庫,導致數據庫壓力激增。解決這個問題的主要思路就是避免多線程并發去重建緩存,因此方案有兩種。

第一種是基于互斥鎖,當發現緩存未命中時需要先獲取互斥鎖,再重建緩存,緩存重建完成釋放鎖。這樣就可以保證緩存重建同一時刻只會有一個線程執行。不過這種做法會導致緩存重建時性能下降嚴重。

第二種是基于邏輯過期,也就是不給熱點Key設置過期時間,而是給數據添加一個過期時間的字段。這樣熱點Key就不會過期,緩存中永遠有數據。

查詢到數據時基于其中的過期時間判斷key是否過期,如果過期開啟獨立新線程異步的重建緩存,而查詢請求先返回舊數據即可。當然,這個過程也要加互斥鎖,但由于重建緩存是異步的,而且獲取鎖失敗也無需等待,而是返回舊數據,這樣性能幾乎不受影響。

需要注意的是,無論是采用哪種方式,在獲取互斥鎖后一定要再次判斷緩存是否命中,做dubbo check. 因為當你獲取鎖成功時,可能是在你之前有其它線程已經重建緩存了。

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

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

相關文章

MySQL訪問控制與賬號管理:原理、技術與最佳實踐

MySQL的安全體系建立在精細的訪問控制和賬號管理機制上。本文基于MySQL 9.3官方文檔,深入解析其核心原理、關鍵技術、實用技巧和行業最佳實踐。 一、訪問控制核心原理:雙重驗證機制 連接驗證 (Connection Verification) 客戶端發起連接時,MySQL依據user_name@host_name組合進…

Go語言爬蟲系列教程4:使用正則表達式解析HTML內容

Go語言爬蟲系列教程4:使用正則表達式解析HTML內容 正則表達式(Regular Expression,簡稱RegEx)是處理文本數據的利器。在網絡爬蟲中,我們經常需要從HTML頁面中提取特定的信息,正則表達式就像一個智能的&quo…

筆記 | docker構建失敗

筆記 | docker構建失敗 構建報錯LOG1 rootThinkPad-FLY:/mnt/e/02-docker/ubunutu-vm# docker build -t ubuntu16.04:v1 . [] Building 714.5s (6/11) docker:default> [internal] load …

CentOS 7.9 安裝 寶塔面板

在 CentOS 7.9 上安裝 寶塔面板(BT Panel) 的完整步驟如下: 1. 準備工作 系統要求: CentOS 7.x(推薦 7.9)內存 ≥ 1GB(建議 2GB)硬盤 ≥ 20GBroot 權限(需使用 root 用戶…

第 86 場周賽:矩陣中的幻方、鑰匙和房間、將數組拆分成斐波那契序列、猜猜這個單詞

Q1、[中等] 矩陣中的幻方 1、題目描述 3 x 3 的幻方是一個填充有 從 1 到 9 的不同數字的 3 x 3 矩陣,其中每行,每列以及兩條對角線上的各數之和都相等。 給定一個由整數組成的row x col 的 grid,其中有多少個 3 3 的 “幻方” 子矩陣&am…

【AI News | 20250604】每日AI進展

AI Repos 1、jaaz Jaaz是一款免費開源的AI設計代理,作為Lovart的本地替代品,它能實現圖像、海報、故事板的設計、編輯和生成。Jaaz集成了LLM,可智能生成提示并批量生成圖像,支持Ollama、Stable Diffusion等本地及API模型。用戶可…

Docker load 后鏡像名稱為空問題的解決方案

在使用 docker load命令從存檔文件中加載Docker鏡像時,有時會遇到鏡像名稱為空的情況。這種情況通常是由于在保存鏡像時未正確標記鏡像名稱和標簽,或者在加載鏡像時出現了意外情況。本文將介紹如何診斷和解決這一問題。 一、問題描述 當使用 docker lo…

SQL進階之旅 Day 14:數據透視與行列轉換技巧

【SQL進階之旅 Day 14】數據透視與行列轉換技巧 開篇 歡迎來到“SQL進階之旅”系列的第14天!今天我們將探討數據透視與行列轉換技巧,這是數據分析和報表生成中的核心技能。無論你是數據庫開發工程師、數據分析師還是后端開發人員,行轉列或列…

haribote原型系統改進方向

在時鐘中斷、計時器和鍵盤輸入方面,一些創新性的改進方向: 時鐘中斷 (PIT / inthandler20) 動態節拍 (Tickless Kernel):當前的 PIT 中斷以固定頻率(約 100Hz)觸發,即使系統空閑或沒有即將到期的計時器&…

LabVIEW基于 DataSocket從 OPC 服務器讀取數據

LabVIEW 中基于 DataSocket 函數從 OPC 服務器讀取數據的功能,為工業自動化等場景下的數據交互提供了解決方案。通過特定函數實現 URL 指定、連接建立與管理、數據讀取,相比傳統 Socket 通信和 RESTful API ,在 OPC 服務器數據交互場景有適配…

SimpleDateFormat 和 DateTimeFormatter 的異同

在Java開發中Date類型轉String類型是比較常見的,其中最常用的是以下幾種方式: 1. 使用SimpleDateFormat(Java 8之前) import java.text.SimpleDateFormat; import java.util.Date;public class DateToStringExample {public sta…

《前端面試題:CSS對瀏覽器兼容性》

CSS瀏覽器兼容性完全指南:從原理到實戰 跨瀏覽器兼容性是前端開發的核心挑戰,也是面試中的高頻考點。查看所有css屬性對各個瀏覽器兼容網站:https://caniuse.com 一、瀏覽器兼容性為何如此重要? 在當今多瀏覽器生態中&#xff0c…

【stm32開發板】單片機最小系統原理圖設計

一、批量添加網絡標簽 可以選擇浮動工具中的N,單獨為引腳添加網絡標簽。 當芯片引腳非常多的時候,選中芯片,右鍵選擇扇出網絡標簽/非連接標識 按住ctrl鍵即可選中多個引腳 點擊將引腳名稱填入網絡名 就完成了引腳標簽的批量添加 二、電源引…

golang連接sm3認證加密(app)

文章目錄 環境文檔用途詳細信息 環境 系統平臺:Linux x86-64 Red Hat Enterprise Linux 7 版本:4.5 文檔用途 golang連接安全版sm3認證加密數據庫,驅動程序詳見附件。 詳細信息 1.下載Linux golang安裝包 go1.17.3.linux-amd64.tar.gz 1.1. 解壓安…

node實例應用

打開vscode,創建node項目,直接進入一個干凈的文件夾,打開控制臺 一 項目初始化 1. 初始化包管理 npm init -y2. 安裝express npm install express4.17.1 3. 根目錄下創建app.js,引入express // 引入expree const express require(express)// 創建實例 const …

Springboot——整合websocket并根據type區別處理

文章目錄 前言架構思想項目結構代碼實現依賴引入自定義注解定義具體的處理類定義 TypeAWebSocketHandler定義 TypeBWebSocketHandler 定義路由處理類配置類,綁定point制定前端頁面編寫測試接口方便跳轉進入前端頁面 測試驗證結語 前言 之前寫過一篇類似的博客&…

vscode命令行debug

vscode命令行debug 一般命令行debug會在遠程連服務器的時候用上,命令行debug的本質是在執行時暴露一個監聽端口,通過進入這個端口,像本地調試一樣進行。 這里提供兩種方式: 直接在命令行中添加debugpy,適用于python…

Hot100 Day02(移動0,乘最多水的容器、三數之和、接雨水)

移動零 題目鏈接 題目描述: 思路:上述藍色箭頭代表當前遍歷的元素,紅色數字則是當前空位0的位置,每一次遇到非0元素,就是講該元素的位置和空位0的位置進行交換,同時空位0的下標1. 代碼 class Solution …

(eNSP)配置WDS手拉手業務

1.實驗拓撲 2.基礎配置 [SW1]dis cu # sysname SW1 # vlan batch 10 100 110 120 # dhcp enable # interface Vlanif10ip address 192.168.10.2 255.255.255.0 # interface Vlanif100ip address 192.168.100.2 255.255.255.0dhcp select interfacedhcp server excluded-ip-add…

lua的筆記記錄

類似python的eval和exec 可以偽裝成其他格式的文件,比如.dll 希望在異常發生時,能夠讓其沉默,即異常捕獲。而在 Lua 中實現異常捕獲的話,需要使用函數 pcall,假設要執行一段 Lua 代碼并捕獲里面出現的所有錯誤&#xf…