黑馬點評【緩存】

目錄

一、為什么要使用緩存

二、添加商戶緩存

1.緩存的模型和思路

2.代碼

3.緩存更新策略

Redis內存淘汰機制:

3.1 被動淘汰策略(不主動淘汰,僅在查詢時觸發)

3.2 主動淘汰策略(主動掃描內存,按規則淘汰數據)

Redis 過期刪除策略

3.3三大過期刪除策略詳解

1.?被動刪除(惰性刪除)

?編輯

2.?主動刪除(定期刪除)

3.?內存淘汰策略的補充作用

三、數據庫緩存不一致解決方案:

1.發生原因及相關解決方案

2.實現商鋪和緩存與數據庫雙寫一致

四、緩存穿透

1.定義

2.編碼解決商品查詢的緩存穿透問題:

3.小總結:

五、緩存雪崩

1.定義

2.解決方案

六、緩存擊穿

1.?定義

2.解決方案

2.1互斥鎖

2.2邏輯過期

3.互斥鎖 vs 邏輯過期:場景選擇與組合方案

4.利用互斥鎖解決緩存擊穿問題

5.利用邏輯過期解決緩存擊穿問題

七、封裝Redis工具類


一、為什么要使用緩存

一句話:因為速度快,好用

緩存數據存儲于代碼中,而代碼運行在內存中,內存的讀寫性能遠高于磁盤,緩存可以大大降低**用戶訪問并發量帶來的**服務器讀寫壓力

實際開發過程中,企業的數據量,少則幾十萬,多則幾千萬,這么大數據量,如果沒有緩存來作為"避震器",系統是幾乎撐不住的,所以企業會大量運用到緩存技術;

但是緩存也會增加代碼復雜度和運營的成本:

實際開發中,會構筑多級緩存來使系統運行速度進一步提升,例如:本地緩存與redis中的緩存并發使用

瀏覽器緩存:主要是存在于瀏覽器端的緩存

應用層緩存:可以分為tomcat本地緩存,比如之前提到的map,或者是使用redis作為緩存

數據庫緩存:在數據庫中有一片空間是 buffer pool,增改查數據都會先加載到mysql的緩存中

CPU緩存:當代計算機最大的問題是 cpu性能提升了,但內存讀寫速度沒有跟上,所以為了適應當下的情況,增加了cpu的L1,L2,L3級的緩存

二、添加商戶緩存

在我們查詢商戶信息時,我們是直接操作從數據庫中去進行查詢的,大致邏輯是這樣,直接查詢數據庫那肯定慢咯,所以我們需要增加緩存

@GetMapping("/{id}")
public Result queryShopById(@PathVariable("id") Long id) {//這里是直接查詢數據庫return shopService.queryById(id);
}

1.緩存的模型和思路

標準的操作方式就是查詢數據庫之前先查詢緩存,如果緩存數據存在,則直接從緩存中返回,如果緩存數據不存在,再查詢數據庫,然后將數據存入redis。

2.代碼

代碼思路:如果緩存有,則直接返回,如果緩存不存在,則查詢數據庫,然后存入redis。

3.緩存更新策略

Redis內存淘汰機制:

Redis 內存淘汰策略
│
├─ 被動淘汰策略(noeviction)
│   └─ 機制:內存不足時拒絕寫操作,讀操作正常
│   └─ 適用:不允許數據丟失的場景
│
└─ 主動淘汰策略│├─ 基于過期時間(僅淘汰設置過期時間的鍵)│   ││   ├─ volatile-lru│   │   └─ 機制:淘汰過期鍵中最久未使用的數據│   │   └─ 適用:熱點數據緩存│   ││   ├─ volatile-ttl│   │   └─ 機制:淘汰過期鍵中剩余時間最短的數據│   │   └─ 適用:時效性強的數據(如限時活動)│   ││   └─ volatile-random│       └─ 機制:隨機淘汰過期鍵│       └─ 適用:數據訪問無規律的場景│└─ 基于數據熱度/大小(淘汰所有鍵)│├─ allkeys-lru│   └─ 機制:淘汰所有鍵中最久未使用的數據│   └─ 適用:通用緩存場景(熱點數據優先)│├─ allkeys-random│   └─ 機制:隨機淘汰所有鍵│   └─ 適用:性能優先、訪問無規律的場景│├─ allkeys-lfu│   └─ 機制:淘汰所有鍵中訪問頻率最低的數據│   └─ 適用:長期高頻訪問數據(如常用功能緩存)│└─ volatile-lfu└─ 機制:淘汰過期鍵中訪問頻率最低的數據└─ 適用:需保留高頻訪問的過期數據場景

Redis 提供了 8 種內存淘汰策略,可分為被動淘汰主動淘汰兩類:

3.1 被動淘汰策略(不主動淘汰,僅在查詢時觸發)
  • noeviction(默認策略)
    • 機制:當內存不足時,拒絕執行所有會導致內存增加的命令(如 set、lpush 等),但讀命令(如 get)仍可正常執行。
    • 應用場景:適用于不允許丟失數據的場景(如緩存與數據庫強一致的場景),但需確保業務能處理寫失敗的情況。

3.2 主動淘汰策略(主動掃描內存,按規則淘汰數據)

主動淘汰策略又分為基于過期時間基于數據熱度 / 大小兩類:

①基于過期時間的淘汰策略

此類策略僅淘汰設置了過期時間的鍵,適合緩存場景:

  • volatile-lru(Least Recently Used)

    • 機制:在過期鍵中,淘汰最長時間未被訪問的鍵。
    • 原理:通過維護 “最近使用” 順序,淘汰不活躍數據,適合熱點數據場景(如用戶行為緩存)。
    • 示例:電商首頁商品緩存,頻繁訪問的商品保留,冷門商品被淘汰。
  • volatile-ttl

    • 機制:在過期鍵中,優先淘汰剩余過期時間最短的鍵。
    • 原理:根據 TTL(Time To Live)值判斷,適合對時效性要求高的數據(如限時活動緩存)。
    • 示例:秒殺活動倒計時緩存,剩余時間短的先淘汰。
  • volatile-random

    • 機制:在過期鍵隨機淘汰數據
    • 特點:實現簡單但缺乏針對性,適用于數據訪問無明顯規律的場景。

②基于數據熱度 / 大小的淘汰策略

此類策略對所有鍵(無論是否設置過期時間)生效:

  • allkeys-lru

    • 機制:在所有鍵中,淘汰最長時間未被訪問的鍵。
    • 應用場景:最常用的策略之一,適合緩存場景(如熱點文章、用戶會話緩存),能有效保留活躍數據。
    • 優化:Redis 通過 “近似 LRU” 算法(采樣少量數據而非全量掃描)平衡性能與準確性。
  • allkeys-random

    • 機制:在所有鍵中隨機淘汰數據。
    • 特點:性能開銷小,但可能淘汰活躍數據,適用于數據訪問無規律且對緩存命中率要求不高的場景。
  • volatile-lfu(Least Frequently Used)

    • 機制:在過期鍵中,淘汰訪問頻率最低的鍵。
    • 原理:通過記錄訪問次數區分 “偶然訪問” 和 “高頻訪問” 數據,避免 LRU 淘汰高頻但近期未訪問的鍵。
    • 示例:新聞類應用中,高頻訪問的熱點新聞即使近期未被訪問也會被保留。
  • allkeys-lfu

    • 機制:在所有鍵中,淘汰訪問頻率最低的鍵。
    • 應用場景:適合長期保留高頻訪問數據,例如用戶高頻使用的功能緩存。
策略淘汰范圍淘汰依據適用場景命中率性能開銷
noeviction所有鍵不淘汰,拒絕寫操作不允許數據丟失的場景
allkeys-lru所有鍵最近最少使用通用緩存場景(熱點數據明顯)
volatile-lru過期鍵最近最少使用僅緩存過期數據的場景
allkeys-lfu所有鍵訪問頻率最低長期高頻訪問數據的場景最高
volatile-ttl過期鍵剩余過期時間最短時效性強的數據(如限時活動)
random 策略對應范圍鍵隨機數據訪問無規律或性能優先的場景

Redis 過期刪除策略

Redis 作為內存型數據庫,需要高效處理過期鍵的刪除,避免無效數據占用內存。其過期刪除策略采用被動刪除 + 主動刪除的混合模式,平衡內存占用與 CPU 開銷:

3.3三大過期刪除策略詳解
1.?被動刪除(惰性刪除)
  • 機制:當客戶端訪問某個鍵時,Redis 會檢查該鍵是否過期,若過期則刪除并返回?nil(空)
  • 優點:不主動消耗 CPU 資源,僅在訪問時觸發,對性能影響最小。
  • 缺點:可能導致過期鍵長時間滯留內存,尤其當鍵未被訪問時。
  • 示例
    # 設定期限為 10 秒的鍵
    redis.setex("key1", 10, "value")  
    # 10 秒后未訪問,key1 仍存在于內存中  
    # 當執行 redis.get("key1") 時,才會觸發刪除并返回 nil
    

圖片來源于小林Coding

2.?主動刪除(定期刪除)
  • 機制Redis 周期性地隨機抽取一部分鍵檢查是否過期,并刪除過期鍵。具體規則:
    • 每個 Redis 服務器每秒執行?hz?次(默認?hz=10,即每秒 10 次)過期掃描。
    • 每次掃描隨機選取?ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP?個鍵(默認 20 個),若過期鍵比例超過 25%,則繼續掃描,直到過期鍵比例降至 25% 以下或掃描次數達到上限。
  • 優點:主動清理過期鍵,減少內存浪費。
  • 缺點:掃描頻率和范圍需平衡,頻率過高會占用 CPU,過低則清理不及時。

?圖片來源于小林Coding

3.?內存淘汰策略的補充作用
  • 當 Redis 內存達到?maxmemory?閾值時,會觸發內存淘汰策略(如?lru/lfu?等),此時即使鍵未過期,也可能被淘汰,作為過期刪除的補充機制。

三、數據庫緩存不一致解決方案:

1.發生原因及相關解決方案

由于我們的緩存的數據源來自于數據庫,而數據庫的數據是會發生變化的,因此,如果當數據庫中數據發生變化,而緩存卻沒有同步,此時就會有一致性問題存在,其后果是:

用戶使用緩存中的過時數據,就會產生類似多線程數據安全問題,從而影響業務,產品口碑等;怎么解決呢?有如下幾種方案

Cache Aside Pattern 人工編碼方式:緩存調用者在更新完數據庫后再去更新緩存,也稱之為雙寫方案

Read/Write Through Pattern : 由系統本身完成,數據庫與緩存的問題交由系統本身去處理

Write Behind Caching Pattern :調用者只操作緩存,其他線程去異步處理數據庫,實現最終一致

對比維度Cache Aside Pattern (雙寫方案)Read/Write Through PatternWrite Behind Caching Pattern
數據一致性最終一致性(可能存在短暫不一致窗口)強一致性(系統保證緩存與數據庫同步)最終一致性(異步同步延遲更大)
性能表現寫操作需兩次IO(數據庫+緩存)寫操作由系統優化處理最佳寫性能(僅操作緩存)
實現復雜性需應用層維護雙寫邏輯需實現底層存儲系統抽象層需處理異步隊列和失敗重試機制
適用場景讀多寫少場景(如商品詳情頁)對一致性要求高的場景(如金融賬戶)寫密集場景(如日志記錄)
主要風險并發寫可能引發臟數據<br>(需配合失效機制)系統設計缺陷會導致全局故障數據丟失風險(異步未完成時宕機)
典型應用電商系統商品信息緩存分布式文件系統元數據管理社交平臺點贊數統計
維護成本中(需處理雙寫異常場景)高(需維護存儲抽象層)高(需維護可靠消息隊列)
數據流動方向應用層雙向控制<br>(DB?緩存)單向流動<br>(DB→緩存)單向流動<br>(緩存→DB)

綜合考慮使用方案一,但是方案一調用者如何處理呢?這里有幾個問題

操作緩存和數據庫時有三個問題需要考慮:

如果采用第一個方案,那么假設我們每次操作數據庫后,都操作緩存,但是中間如果沒有人查詢,那么這個更新動作實際上只有最后一次生效,中間的更新動作意義并不大,我們可以把緩存刪除,等待再次查詢時,將緩存中的數據加載出來

?1.刪除緩存還是更新緩存?
??
? 更新緩存:每次更新數據庫都更新緩存,無效寫操作較多
? 刪除緩存:更新數據庫時讓緩存失效,查詢時再更新緩存
2.如何保證緩存與數據庫的操作的同時成功或失敗?
??
? 單體系統,將緩存與數據庫操作放在一個事務
? 分布式系統,利用TCC等分布式事務方案

應該具體操作緩存還是操作數據庫,我們應當是先操作數據庫,再刪除緩存,原因在于,如果你選擇第一種方案,在兩個線程并發來訪問時,假設線程1先來,他先把緩存刪了,此時線程2過來,他查詢緩存數據并不存在,此時他寫入緩存,當他寫入緩存后,線程1再執行更新動作時,實際上寫入的就是舊的數據,新的數據被舊數據覆蓋了。

先操作緩存還是先操作數據庫?
? 先刪除緩存,再操作數據庫
? 先操作數據庫,再刪除緩存

2.實現商鋪和緩存與數據庫雙寫一致

核心思路如下:

修改ShopController中的業務邏輯,滿足下面的需求:

根據id查詢店鋪時,如果緩存未命中,則查詢數據庫,將數據庫結果寫入緩存,并設置超時時間

根據id修改店鋪時,先修改數據庫,再刪除緩存

修改重點代碼1:修改ShopServiceImpl的queryById方法

設置redis緩存時添加過期時間

修改重點代碼2

代碼分析:通過之前的淘汰,我們確定了采用刪除策略,來解決雙寫問題,當我們修改了數據之后,然后把緩存中的數據進行刪除,查詢時發現緩存中沒有數據,則會從mysql中加載最新的數據,從而避免數據庫和緩存不一致的問題

四、緩存穿透

1.定義

請求查詢一個不存在的數據,緩存和數據庫中均無該數據,導致請求每次都穿透到數據庫,造成資源浪費。若被惡意攻擊(如批量請求不存在的 ID),可能導致數據庫崩潰。

典型場景:惡意用戶通過腳本批量請求user_id=-1等不存在的參數。

例如:請求店鋪為0的信息

常見的解決方案有兩種:

方案實現方式優缺點
布隆過濾器(Bloom Filter)在請求進入數據庫前,先用布隆過濾器判斷數據是否存在,不存在則直接拒絕。- 優點:空間效率高,過濾速度快
- 缺點:存在誤判率(誤報存在,漏報不存在)
空值緩存當查詢結果為 null 時,仍將 null 存入緩存(設置短過期時間,如 5 分鐘),避免重復查詢數據庫。- 優點:簡單易實現
- 缺點:可能存儲無效空值,占用少量內存

2.編碼解決商品查詢的緩存穿透問題:

核心思路如下:

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

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

3.小總結:

緩存穿透產生的原因是什么?

用戶請求的數據在緩存中和數據庫中都不存在,不斷發起這樣的請求,給數據庫帶來巨大壓力

緩存穿透的解決方案有哪些?

* 緩存null值
* 布隆過濾

* 增強id的復雜度,避免被猜測id規律
* 做好數據的基礎格式校驗
* 加強用戶權限校驗
* 做好熱點參數的限流

五、緩存雪崩

1.定義

  • 大量緩存 key 在同一時間段失效,或緩存服務整體不可用,導致海量請求直接訪問數據庫,造成數據庫雪崩甚至宕機。
  • 典型場景:緩存服務器重啟、大量 key 設置相同過期時間(如凌晨零點失效)。

2.解決方案

方案實現方式優缺點
過期時間分散化為不同 key 的過期時間添加隨機值(如TTL=3600+random(0,1800)),避免集體失效。- 優點:簡單有效,成本低
- 缺點:無法應對緩存服務整體故障
多級緩存架構部署多層緩存(如本地緩存 + Redis 緩存),當 Redis 失效時,本地緩存仍可響應請求。- 優點:提高可用性,降低數據庫壓力
- 缺點:維護成本高,需處理緩存一致性
緩存集群與熔斷- 緩存采用集群部署,避免單點故障
- 引入熔斷機制(如 Hystrix),當數據庫壓力過高時暫時拒絕部分請求。
- 優點:高可用性,保護數據庫
- 缺點:需要額外組件支持,增加架構復雜度
持久化與熱重啟啟用 Redis 持久化(RDB/AOF),服務器重啟時快速加載緩存,減少全量失效窗口。- 優點:恢復速度快
- 缺點:依賴持久化文件的完整性

六、緩存擊穿

1.?定義

  • 熱點數據的緩存過期瞬間,大量請求同時穿透緩存直達數據庫,導致數據庫負載激增的現象。
  • 典型場景:秒殺活動中,某商品的緩存 key 過期,瞬間數萬請求訪問數據庫查詢庫存。

?假設線程1在查詢緩存之后,本來應該去查詢數據庫,然后把這個數據重新加載到緩存的,此時只要線程1走完這個邏輯,其他線程就都能從緩存中加載這些數據了,但是假設在線程1沒有走完的時候,后續的線程2,線程3,線程4同時過來訪問當前這個方法, 那么這些線程都不能從緩存中查詢到數據,那么他們就會同一時刻來訪問查詢緩存,都沒查到,接著同一時間去訪問數據庫,同時的去執行數據庫代碼,對數據庫訪問壓力過大

2.解決方案

方案實現方式優缺點
互斥鎖(Mutex)當緩存失效時,先通過 Redis 的SETNX獲取鎖,成功的線程查詢數據庫并更新緩存,其他線程等待鎖釋放。- 優點:簡單有效,避免大量請求擊穿
- 缺點:存在鎖競爭,可能阻塞請求
熱點數據永不過期(邏輯過期)不設置過期時間,通過后臺線程異步更新數據(如定時任務)。- 優點:完全避免過期擊穿
- 缺點:數據一致性略有延遲,需配合版本號或消息隊列更新
緩存時間隨機打散給熱點 key 的過期時間添加隨機偏移(如expire=3600+random(0,600)),避免同時過期。- 優點:輕量級方案,適合非強一致場景
- 缺點:無法完全杜絕擊穿,但可大幅降低概率

這里我們重點講解互斥鎖和邏輯過期

2.1互斥鎖

因為鎖能實現互斥性。假設線程過來,只能一個人一個人的來訪問數據庫,從而避免對于數據庫訪問壓力過大,但這也會影響查詢的性能,因為此時會讓查詢的性能從并行變成了串行,我們可以采用tryLock方法 + double check來解決這樣的問題。

假設現在線程1過來訪問,他查詢緩存沒有命中,但是此時他獲得到了鎖的資源,那么線程1就會一個人去執行邏輯,假設現在線程2過來,線程2在執行過程中,并沒有獲得到鎖,那么線程2就可以進行到休眠,直到線程1把鎖釋放后,線程2獲得到鎖,然后再來執行邏輯,此時就能夠從緩存中拿到數據了。

優點缺點
1. 強一致性,確保緩存與數據庫實時同步;
2. 實現簡單,適用于中小并發場景。
1. 高并發下存在鎖競爭,可能導致請求延遲;
2. 極端情況下(如數據庫慢查詢)可能引發線程饑餓。

2.2邏輯過期

  • 物理過期:緩存不設置expire,永久存儲;
  • 邏輯過期:在緩存值中附加過期時間戳,由應用層判斷是否需要更新。
  • 異步更新:發現邏輯過期時,啟動后臺線程異步更新緩存,前臺請求返回舊數據,避免阻塞。

流程:

我們把過期時間設置在 redis的value中,注意:這個過期時間并不會直接作用于redis,而是我們后續通過邏輯去處理。假設線程1去查詢緩存,然后從value中判斷出來當前的數據已經過期了,此時線程1去獲得互斥鎖,那么其他線程會進行阻塞,獲得了鎖的線程他會開啟一個線程去進行 以前的重構數據的邏輯,直到新開的線程完成這個邏輯后,才釋放鎖, 而線程1直接進行返回,假設現在線程3過來訪問,由于線程線程2持有著鎖,所以線程3無法獲得鎖,線程3也直接返回數據,只有等到新開的線程2把重建數據構建完后,其他線程才能走返回正確的數據。

這種方案巧妙在于,異步的構建緩存,缺點在于在構建完緩存之前,返回的都是臟數據。

優點缺點
1. 無鎖競爭,響應速度快,適合高并發讀場景;
2. 避免緩存擊穿的同時保證服務可用性。
1. 存在數據不一致窗口(舊數據可能被讀取);
2. 后臺更新失敗時需額外容錯機制(如重試隊列)。

3.互斥鎖 vs 邏輯過期:場景選擇與組合方案

維度互斥鎖方案邏輯過期方案
一致性要求強一致性(實時同步)最終一致性(允許短期延遲)
并發場景中小并發、寫操作較頻繁高并發讀、寫操作較少
典型應用庫存扣減、訂單狀態查詢商品詳情頁、首頁輪播圖
組合優化互斥鎖 + 邏輯過期
1. 邏輯過期作為主方案,提升讀性能;
2. 后臺更新時添加互斥鎖,避免并發臟寫。

互斥鎖方案:由于保證了互斥性,所以數據一致,且實現簡單,因為僅僅只需要加一把鎖而已,也沒其他的事情需要操心,所以沒有額外的內存消耗,缺點在于有鎖就有死鎖問題的發生,且只能串行執行性能肯定受到影響

邏輯過期方案:線程讀取過程中不需要等待,性能好,有一個額外的線程持有鎖去進行重構數據,但是在重構數據完成前,其他的線程只能返回之前的數據,且實現起來麻煩

4.利用互斥鎖解決緩存擊穿問題

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

如果獲取到了鎖的線程,再去進行查詢,查詢后將數據寫入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;}

5.利用邏輯過期解決緩存擊穿問題

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

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

如果封裝數據:因為現在redis中存儲的數據的value需要帶上過期時間,此時要么你去修改原來的實體類,要么你

步驟一、

新建一個實體類,我們采用第二個方案,這個方案,對原來代碼沒有侵入性。

package com.hmdp.utils;import lombok.Builder;
import lombok.Data;import java.io.Serializable;
import java.time.LocalDateTime;@Data
@Builder
public class RedisData implements Serializable {private LocalDateTime expireTime;private Object data;
}

步驟二、

在ShopServiceImpl新增此方法,利用單元測試進行緩存預熱

在測試類中

步驟三:正式代碼

ShopServiceImpl

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;
}

七、封裝Redis工具類

基于StringRedisTemplate封裝一個緩存工具類,滿足下列需求:

* 方法1:將任意Java對象序列化為json并存儲在string類型的key中,并且可以設置TTL過期時間
* 方法2:將任意Java對象序列化為json并存儲在string類型的key中,并且可以設置邏輯過期時間,用于處理緩存擊穿問題

* 方法3:根據指定的key查詢緩存,并反序列化為指定類型,利用緩存空值的方式解決緩存穿透問題
* 方法4:根據指定的key查詢緩存,并反序列化為指定類型,需要利用邏輯過期解決緩存擊穿問題

將邏輯進行封裝

@Slf4j
@Component
public class CacheClient {private final StringRedisTemplate stringRedisTemplate;private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);public CacheClient(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}public void set(String key, Object value, Long time, TimeUnit unit) {stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);}public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {// 設置邏輯過期RedisData redisData = new RedisData();redisData.setData(value);redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));// 寫入RedisstringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));}public <R,ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit){String key = keyPrefix + id;// 1.從redis查詢商鋪緩存String json = stringRedisTemplate.opsForValue().get(key);// 2.判斷是否存在if (StrUtil.isNotBlank(json)) {// 3.存在,直接返回return JSONUtil.toBean(json, type);}// 判斷命中的是否是空值if (json != null) {// 返回一個錯誤信息return null;}// 4.不存在,根據id查詢數據庫R r = dbFallback.apply(id);// 5.不存在,返回錯誤if (r == null) {// 將空值寫入redisstringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);// 返回錯誤信息return null;}// 6.存在,寫入redisthis.set(key, r, time, unit);return r;}public <R, ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {String key = keyPrefix + 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);R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);LocalDateTime expireTime = redisData.getExpireTime();// 5.判斷是否過期if(expireTime.isAfter(LocalDateTime.now())) {// 5.1.未過期,直接返回店鋪信息return r;}// 5.2.已過期,需要緩存重建// 6.緩存重建// 6.1.獲取互斥鎖String lockKey = LOCK_SHOP_KEY + id;boolean isLock = tryLock(lockKey);// 6.2.判斷是否獲取鎖成功if (isLock){// 6.3.成功,開啟獨立線程,實現緩存重建CACHE_REBUILD_EXECUTOR.submit(() -> {try {// 查詢數據庫R newR = dbFallback.apply(id);// 重建緩存this.setWithLogicalExpire(key, newR, time, unit);} catch (Exception e) {throw new RuntimeException(e);}finally {// 釋放鎖unlock(lockKey);}});}// 6.4.返回過期的商鋪信息return r;}public <R, ID> R queryWithMutex(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {String key = keyPrefix + id;// 1.從redis查詢商鋪緩存String shopJson = stringRedisTemplate.opsForValue().get(key);// 2.判斷是否存在if (StrUtil.isNotBlank(shopJson)) {// 3.存在,直接返回return JSONUtil.toBean(shopJson, type);}// 判斷命中的是否是空值if (shopJson != null) {// 返回一個錯誤信息return null;}// 4.實現緩存重建// 4.1.獲取互斥鎖String lockKey = LOCK_SHOP_KEY + id;R r = null;try {boolean isLock = tryLock(lockKey);// 4.2.判斷是否獲取成功if (!isLock) {// 4.3.獲取鎖失敗,休眠并重試Thread.sleep(50);return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);}// 4.4.獲取鎖成功,根據id查詢數據庫r = dbFallback.apply(id);// 5.不存在,返回錯誤if (r == null) {// 將空值寫入redisstringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);// 返回錯誤信息return null;}// 6.存在,寫入redisthis.set(key, r, time, unit);} catch (InterruptedException e) {throw new RuntimeException(e);}finally {// 7.釋放鎖unlock(lockKey);}// 8.返回return r;}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);}
}

看到這如果有用的話記得點贊關注哦,后續會更新更多內容的!!

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

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

相關文章

【地圖服務限制范圍】

根據你提供的 eb_service_area 表結構&#xff0c;其中有兩個字段與地理坐標相關&#xff1a;latlng 和 limit_latlng。這兩個字段分別用于存儲服務范圍區域的坐標和限制區域的坐標。下面是對這兩個字段的具體分析以及如何使用它們來定義執行范圍。 字段分析 latlng&#xff0…

python數據結構和算法(1)

數據結構和算法簡介 數據結構&#xff1a;存儲和組織數據的方式&#xff0c;決定了數據的存儲方式和訪問方式。 算法&#xff1a;解決問題的思維、步驟和方法。 程序 數據結構 算法 算法 算法的獨立性 算法是獨立存在的一種解決問題的方法和思想&#xff0c;對于算法而言&a…

Linux操作系統-性能優化

1. 基礎工具 top / htop top # 實時查看CPU、內存、進程 htop # 增強版&#xff08;支持鼠標操作&#xff09; 關鍵指標&#xff1a;%CPU&#xff08;CPU占用&#xff09;、%MEM&#xff08;內存占用&#xff09;、LOAD AVERAGE&#xff08;系統負載&#…

如何徹底解決緩存擊穿、緩存穿透、緩存雪崩

一、緩存擊穿 成因&#xff1a;緩存擊穿通常發生在某個熱點數據失效或清空后&#xff0c;大量請求同時涌入后端數據庫&#xff0c;導致數據庫崩潰或宕機。 解決方案&#xff1a; 互斥鎖&#xff1a;在獲取數據時&#xff0c;使用分布式鎖&#xff08;如Redis的分布式鎖&…

JDK 8、JDK 17和JDK 19綜合對比分析

JDK 8、JDK 17和JDK 19在性能、特性、易用性及普及性等方面的綜合對比分析&#xff0c;結合了各版本的核心改進和實際應用場景 目錄 ? 一、性能對比 ? 二、語言與特性演進 &#x1f6e0;? 三、API與功能增強 &#x1f3af; 四、易用性改進 &#x1f4ca; 五、市場普及…

Vue-理解 vuex

一、前言 在開發中大型 Vue 應用時&#xff0c;我們常常會遇到多個組件之間共享數據、通信復雜的問題。例如&#xff1a; 多個組件需要訪問同一個用戶信息&#xff1b;組件之間需要傳遞狀態或事件&#xff1b;數據變更需要同步更新多個組件&#xff1b; 這時&#xff0c;Vue…

【209】VS2022 C++對排好序的vector使用二分查找算法的例子

本文介紹了如何對已經排序的 vector 進行二分法查找。 首先&#xff0c;我們先看一下存儲數據的類&#xff0c;我們假設所有數據的 id 是唯一的&#xff1a; DataItem.h #pragma once #include<string>namespace zc {class DataItem{public:int m_id;std::string m_na…

ABAP 上傳 excel 報表

&#xff08;1&#xff09;先在屏幕上增加上傳文件的按鈕 "屏幕選擇條件" SELECTION-SCREEN BEGIN OF BLOCK b1 WITH FRAME TITLE TEXT-001. PARAMETERS : p_source LIKE rlgrap-filename . SELECTION-SCREEN END OF BLOCK b1. 你會發現&#xff0c;上面的代碼只…

Compose與View系統互操作方案

本文將全面解析 Android 現代 UI 框架 Jetpack Compose 與傳統 View 系統的互操作方案&#xff0c;涵蓋基礎原理、實戰技巧、性能優化和高級應用&#xff0c;助你實現漸進式遷移和混合開發。 一、互操作的必要性與整體架構 1.1 為什么需要互操作性 漸進式遷移&#xff1a;大型…

HNCTF 2025 Just Ping Write-up

part 1 路由部分主邏輯逆向 package mainimport ("net/http" )func main() {// 注冊路由和處理函數// 當訪問 "/api/ping" 路徑時&#xff0c;調用 pingHandler 函數處理請求http.HandleFunc("/api/ping", pingHandler)// 注冊開發測試API路由//…

OpenCV CUDA模塊中用于稠密光流計算的 TV-L1(Dual TV-L1)算法類cv::cuda::OpticalFlowDual_TVL1

操作系統&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 編程語言&#xff1a;C11 算法描述 cv::cuda::OpticalFlowDual_TVL1類是基于變分優化方法的稠密光流算法實現&#xff08;Dual TV-L1 光流模型&#xff09;&#xff0c;在 GPU 上加…

ThreadPoolTaskExecutor+CompletableFuture實現多線程異步數據同步和自定義線程池監控和動態調整實現

前言 ThreadPoolTaskExecutor是Spring框架提供的一個線程池實現&#xff0c;它是對Java標準庫中ThreadPoolExecutor的封裝&#xff0c;提供了更便捷的配置和集成方式&#xff0c;特別適合在Spring環境中使用。相關線程池概念見線程&線程池相關 CompletableFuture 是 Java…

一篇文章理解js閉包和作用于原理

一、js閉包的作用原理 JS閉包是指內部函數訪問外部函數變量的機制&#xff0c;常用于數據封裝和模塊化。典型應用包括創建私有變量、解決循環中的異步問題、實現函數柯里化等。案例分析展示了閉包在計數器、防抖函數等場景的使用&#xff0c;同時揭示了可能的內存泄漏風險。正…

GUI絲滑教程-python tinker

在 Tkinter GUI 應用中&#xff0c;線程可以幫助你在后臺執行長時間運行的任務&#xff0c;而不阻塞界面響應。下面是一些技巧&#xff0c;幫助你在使用線程時避免 Tkinter 界面卡頓的問題。 為什么 Tkinter 界面會卡頓&#xff1f; Tkinter 使用 主線程 來處理 UI 更新&…

第一部分-數據通信網絡基礎

目錄 一、什么是網絡通信&#xff1f; 二、網絡通信設備的基本識別 1.雙絞線 2.集線器&#xff08;物理層設備&#xff09; 3.中繼器&#xff08;物理層設備&#xff09; 4.接入交換機 5.匯聚交換機 6.核心交換機 7.路由器 8.無線路由器 9.光貓 一、什么是網絡通信&#xff1f;…

windows電腦解決筆記本搜索不到wifi問題

windows筆記本電腦明明打開了wifi功能&#xff0c;卻搜索不到wifi&#xff0c;此問題可能是網絡適配器被禁用的原因導致&#xff0c;通過以下方法也許能解決&#xff0c;無需重啟電腦 1、右鍵點擊網絡或wifi圖標&#xff0c;打開界面”網絡和internet“ 2、選擇”高級網絡設置…

C# 界面檢測顯示器移除并在可用顯示器上顯示

C# 檢測顯示器被移除&#xff0c;將界面在當前可用的顯示器上顯示&#xff0c;避免程序在任務欄點擊無響應。 using System; using System.Linq; using System.Windows.Forms;public class MonitorWatcher : IDisposable {private readonly Form _targetForm;private Screen …

JAVA實戰開源項目:青年公寓服務平臺 (Vue+SpringBoot) 附源碼

本文項目編號 T 233 &#xff0c;文末自助獲取源碼 \color{red}{T233&#xff0c;文末自助獲取源碼} T233&#xff0c;文末自助獲取源碼 目錄 一、系統介紹二、數據庫設計三、配套教程3.1 啟動教程3.2 講解視頻3.3 二次開發教程 四、功能截圖五、文案資料5.1 選題背景5.2 國內…

阿里云服務狀態監控:實時掌握云服務健康狀況

前言 在云計算時代,企業和開發者越來越依賴云服務提供商的基礎設施和服務。當我們的應用部署在云上,服務的可用性和穩定性就與云服務提供商息息相關。一旦云服務出現故障或維護,可能會對我們的業務造成直接影響。因此,實時了解云服務的運行狀態變得尤為重要。阿里云作為國…

使用VSCode開發FastAPI指南

1概述 FastAPI 是一個現代的高性能 Web 框架&#xff0c;用于使用 Python 構建 API。它旨在讓開發者輕松快速高效地構建 API&#xff0c;同時提供 API 的自動驗證、序列化和文檔記錄等功能&#xff0c;使其成為構建 Web 服務和微服務的熱門選擇。 在這個 FastAPI 教程中&#…