redis緩存擊穿問題解決

網上看了很多解決緩存擊穿的方案,
我覺得不夠好,自己總結了一番
本文儘量使用大白話,儘量不寫代碼,請認真讀
希望能讓你們滿意


1. redis的緩存擊穿是什麼?

如果我有一個業務,需要查詢數據庫,這個查詢很耗時,
且業務上來看這個要非常頻繁的取查詢它
那麼通常我可以把查詢的結果保存在redis,設置一個符合業務的過期時間
然後以後的查詢都直接查redis
redis的高QPS特性,可以很好的解決查數據庫很慢的問題


隱患:
如果我們系統的併發很高,
在某個時間節點,突然緩存失效,這時候有大量的請求打過來
那麼由於redis沒有緩存數據,這時候我們的請求會全部去查一遍數據庫
這時候我們的數據庫服務會面臨非常大的風險,要麼連接被佔滿
要麼其他業務不可用
這種情況就是redis的緩存擊穿


2. 如何解決緩存擊穿

解決緩存擊穿換句話說,就是要保證最終落在硬盤上的查詢操作要可控
注意這裏使用的是可控,而不是少

2.1 普通的redis緩存使用方式

先來看看普通的redis緩存設計

2.1.1 使用redis緩存

  • 比較簡單的設計,可以在數據需要的時候再加載

查詢邏輯

在這裏插入圖片描述
由此圖可以看出,查詢是隻有兩種情況的:

  1. 緩存命中,直接返回緩存結果
  2. 緩存未命中,查數據庫,再返回結果

優點

  • 設計簡單,開發效率高
  • 不會侵入業務代碼,spring的aop就能很好的實現

缺點

  • 一旦redis的數據失效,那麼假如這時候有10w個請求打過來,由於redis沒有緩存,那麼按照緩存設計的邏輯,就會全部去查數據庫

適用情況

  • 如果一個系統是內部系統,天生沒有太多的用戶量,而某個接口需要非常耗時的查詢,而數據變化又不會太頻繁,就比較適用這種設計, 沒有太多用戶,就不會造成緩存失效的時候大量請求壓到數據庫的問題

2.2 解決redis緩存擊穿問題

下面我們來看看有哪些設計可以解決緩存擊穿的問題
下面的設計都解決了緩存擊穿的問題
只是他們的設計各有利弊,我們需要在不同情況下來使用

2.2.1 主動刷新緩存設計

如果我們的數據是保存在redis緩存中,而redis緩存失效之後一定會查數據庫
那麼我們是否可以主動出擊,在redis緩存失效之前,主動查數據庫?

查詢邏輯

  1. 先將所有可能查詢到的數據存入redis
  2. 對redis中的數據庫定時更新,保證redis永遠都會有數據存在
  3. 來請求只查redis
    在這裏插入圖片描述

優點

  1. 用戶的請求壓力永遠不會直接打到數據庫上
  2. 查詢效率很高

缺點

  1. 可能對redis內存消耗非常大,因爲要提前將數據加載到緩存
  2. 增加了系統的複雜度,必須要有個非常可靠的定時任務操作,不然一旦定時任務失效,那麼redis中的數據失效,對於用戶來說就是服務不可用
  3. 數據的實時性非常依賴定時任務的執行頻率,定時任務執行的頻率高,實時性就強
  • 如果刷新緩存的間隔設置很長,那麼數據實時性就不夠好,
  • 如果刷新緩存的間隔很短,那麼頻繁的全量刷數據庫到緩存對系統和數據庫都是壓力,也會讓數據庫和應用服務器的負載變得不夠平穩
  1. 由於是隻查詢緩存,所以會對業務代碼進行較大程度的改動,後期業務變化,可能會非常難以維護

適用情況

符合以下條件,那麼我們可以使用這種設計

  1. 已有一套現成的高可靠分佈式定時任務系統
  2. 查詢的數據變化不大
  3. 用戶的請求量非常大的情況下

2.2.2 使用redis的分佈式鎖

對 2.1.1 使用redis緩存 的設計進行一些改動
讓我們對數據庫的重複查詢操作變爲1次
既然我們使用了redis,那麼可以利用redis實現的分佈式鎖setnx 來實現互斥的數據庫操作
在這裏插入圖片描述

查詢邏輯

  1. 如果緩存命中直接返回數據集
  2. 如果緩存沒有,則嘗試獲取分佈式鎖(有超時設置)
  3. 如果沒有拿到鎖,則阻塞當前線程,n秒,之後再次嘗試獲取分佈式鎖(自旋,輪詢,浪費CPU)
  4. 拿到鎖之後檢查數據是否已經被其他線程放到redis緩存中,如果redis緩存已有,直接返回redis中的數據,釋放分佈式鎖
  5. 如果緩存沒有被刷新,則查數據庫
  6. 將數據庫查詢的結果保存到redis緩存中
  7. 返回查詢結果

優點

  1. 數據的實時性較高
  2. 不需要其他外部系統依賴,利用了redis自己的特性,實現分佈式鎖
  3. 保證了同樣的數據庫查詢同時只會查詢1次,對數據庫的壓力較小
  4. 不會侵入業務代碼,spring的aop就能很好的實現

缺點

  1. 由於阻塞等待分佈式鎖是個自旋阻塞操作,所以其實對應用服務器來說非常浪費cpu的分片時間
  2. 如果這時候大量請求打過來, 應用服務器反而會先扛不住,因爲這裏會有大量的線程在自旋佔用CPU
  3. 如果用戶的查詢是由多個系統的結果構成,每個系統的查詢依賴上一個系統查詢的結果,各個查詢是串行的,那麼自旋的睡眠時間可能會成爲拖慢請求的罪魁禍首,多個系統都這麼設計都在自旋睡眠,明顯效率很低

適用情況

這種方法也是網上給的最多的方法
如果要求保證數據庫的壓力特別小,同樣的請求只能查詢一次數據庫,
而且服務器較多,足以將多個請求分散到不同服務器,不至於造成太多線程自旋,
那麼可以使用這樣的設計,但不推薦,因爲這種自旋操作真的不是個好設計

2.2.3 普通加jvm的鎖查詢緩存

上面分佈式鎖自旋的方法,真的不優雅
這時候我們需要反問一下自己,每個請求,真的只能查詢一次數據庫嗎?數據庫的壓力已經大到如此地步了嗎?
如果不是
那麼下面還有更加合適的設計
不再強求相同的查詢只能查一次數據庫

查詢邏輯

在這裏插入圖片描述

  1. 如果緩存命中直接返回數據集
  2. 如果緩存沒有,則嘗試JVM鎖,其他線程阻塞
  3. 拿到鎖之後,檢查redis是否有數據,以免其他線程已經刷過緩存
  4. 如果redis已經有數據,直接返回,並釋放鎖,返回數據庫結束
  5. 如果redis沒有數據,則查詢數據庫,並保存到redis緩存中
  6. 返回數據,釋放鎖

設有s臺服務器,用戶請求數爲n
那麼同一時間參數相同的請求最多隻會有s次查詢打到數據庫上,這裏s這個常量
相當於原來對於數據庫來說一個O(n)的操作時間下降到了O(s)
這裏可以看出,查詢數據庫操作的耗時與n的增長無關,只與s有關

想象一下,我們有4臺服務器,本來打到數據庫上可能有10w個查詢,但是因爲我們使用了jvm的鎖,每臺服務器只會查詢一次,總的數據庫查詢次數下降到了4次,是不是很高效?而且jvm提供的鎖一定比redis分佈式鎖自旋輪詢高效太多!

優點

  1. 數據的實時性較高
  2. 相對於使用redis分佈式鎖,大幅降低服務器資源的消耗,jvm的鎖效率要高很多
  3. 對於數據庫的消耗較小,是一個和服務器數量s相關的耗時操作,與請求數量n無關(n可能會很大,十萬,百萬級別,而s可能最多兩位數)
  4. 如果mysql數據庫版本較低,說不定還能利用上mysql數據庫的緩存,如果是個不頻繁更新的表,運氣好的情況下s-1次的查詢可能都會命中mysql的緩存
  5. 實現的複雜度低
  6. 不會侵入業務代碼,spring的aop就能很好的實現

缺點

  1. 對數據庫查詢雖然減小到了一個只與服務器數量相關的函數,但依然有冗餘(其實也還好了)

適用情況

  • 真的需要強求,所有服務器只查一次緩存嗎?
  • 如果能容忍較少次數的數據庫重複查詢
  • 這種設計就用這種就已經能很好的解決緩存穿透的問題了,而且設計簡單複雜度低
  • 複雜度低意味着系統的穩定

2.2.4 多級緩存

如果寧非要強求,數據庫同一時間不能收到重複的查詢,那麼也不是沒有辦法,往下看

查詢邏輯

在這裏插入圖片描述
查詢的邏輯看圖吧,我懶得一步一步說了,一圖勝錢言
二級緩存的關鍵在於:

  • jvm的緩存時間是個隨機值,比如 10秒~30秒
  • 這種設計,服務器只會在jvm緩存失效,且redis緩存也失效的情況下才會查詢數據庫
  • 而多個服務器的jvm緩存失效時間是隨機值,所以很大程度上避免的同時失效去查庫的情況
  • 由於所有服務器jvm緩存同時失效redis緩存也失效的可能性極低,所以數據庫上重複的查詢會很少
  • (不一定是jvm緩存和jvm的鎖啊,python,go同理)

設服務器的臺數爲s

  • 如何讓O(s)的問題其變爲O(1)呢?其實也是有辦法的,就是多級緩存
  • 就是讓每臺服務器上加一個jvm的緩存在redis之前
  • 這個jvm的緩存時間需要設置一個隨機值,比如 緩存時間爲 5s-10s,這樣可以很大程度避免在redis失效的時候,每臺服務器都需要去做更新redis緩存的操作,因爲每個服務器的jvm緩存失效時間是不一樣的

優點

  1. 數據的實時性較高 (設置合適的jvm緩存過期時間和redis緩存過期時間)
  2. 幾乎沒有冗餘的數據庫查詢
  3. 絕大多數查詢是使用的jvm緩存,效率極高
  4. 對cpu的佔用很低
  5. 不會侵入業務代碼,spring的aop就能很好的實現

缺點

  1. 如果查詢的參數離散度較高,其實會很浪費業務服務器的內存空間(但是可以通過減少jvm緩存的時間來優化一點)
  2. 設計稍微有點複雜,需要有經驗的碼畜來實現

適用情況

幾乎所有情況,強力推薦,我也是這麼做的

2.2.5 其他注意點

以上的這些設計,只是在正常的高併發情況下
如果你的服務器遭遇到了DOS攻擊,那什麼緩存策略都沒用,因爲遲早會把你線程喫滿,然後服務器不可用
這時候你只能在網關或者nginx做一些對ip限流的措施,設置閾值,防止惡意調用接口

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章