Stories

Detail Return Return

Redis是如何進行內存管理的?緩存中有哪些常見問題?如何實現分佈式鎖? - Stories Detail

Redis內存管理

Redis的內存用完了會怎樣?

如果達到設置的上限,Redis的寫命令會返回錯誤信息(但是讀命令還可以正常返回)。

也可以配置內存淘汰機制,當Redis達到內存上限時會沖刷掉舊的內容。

Redis如何做內存優化?

可以好好利用Hash,list,sorted set,set等集合類型數據,因為通常情況下很多小的Key-Value可以用更緊湊的方式存放到一起。儘可能使用散列表(hashes),散列表(是説散列表裏面存儲的數少)使用的內存非常小,所以你應該儘可能的將你的數據模型抽象到一個散列表裏面。比如你的web系統中有一個用户對象,不要為這個用户的名稱,姓氏,郵箱,密碼設置單獨的key,而是應該把這個用户的所有信息存儲到一張散列表裏面。

過期key的刪除策略?

詳情請查看:過期刪除策略

  1. 惰性刪除。在訪問key時,如果發現key已經過期,那麼會將key刪除。
  2. 定時刪除。定時清理key,每次清理會依次遍歷所有DB,從db隨機取出20個key,如果過期就刪除,如果其中有5個key過期,那麼就繼續對這個db進行清理,否則開始清理下一個db。

定期刪除策略原理?

Redis內部維護一個定時任務,默認每秒進行10次(也就是每隔100毫秒一次)過期掃描,過期掃描不會遍歷過期字典中所有的 key,而是採用了一種簡單的貪心策略。

  1. 從過期字典中隨機取出20個key
  2. 刪除這 20 個 key 中已經過期的 key;
  3. 如果這20個key中過期key的比例超過了25%,則重複步驟1

為了保證過期掃描不會出現循環過度,導致線程卡死現象,算法還增加了掃描時間的上限,默認不會超過 25ms。

為什麼定期刪除不是把所有過期 key 都刪除呢?

這樣會對性能造成太大的影響。如果我們 key 數量非常龐大的話,挨個遍歷檢查是非常耗時的,會嚴重影響性能。Redis 設計這種策略的目的是為了平衡內存和性能。

過期key對持久化的影響

RDB:

  • 生成rdb文件:生成時,程序會對key進行檢查,過期key不放入rdb文件。
  • 載入rdb文件:載入時,如果以主服務器模式運行,程序會對文件中保存的key進行檢查,未過期的key會被載入到數據庫中,而過期key則會忽略;如果以從服務器模式運行,無論鍵過期與否,均會載入數據庫中,過期key會通過與主服務器同步而刪除。

AOF:

  • 當服務器以AOF持久化模式運行時,如果數據庫中的某個key已經過期,但它還沒有被惰性刪除或者定期刪除,那麼AOF文件不會因為這個過期key而產生任何影響
  • 當過期key被惰性刪除或者定期刪除之後,程序會向AOF文件追加一條DEL命令,來顯示的記錄被該key已經發被刪除
  • 在執行AOF重寫的過程中,程序會對數據庫中的key進行檢查,已過期的key不會被保存到重寫後的AOF文件中

主從複製:當服務器運行在複製模式下時,從服務器的過期刪除動作由主服務器控制:

  • 主服務器在刪除一個過期key後,會顯式地向所有從服務器發送一個del命令,告知從服務器刪除這個過期key;
  • 從服務器在執行客户端發送的讀命令時,即使碰到過期key也不會將過期key刪除,而是繼續像處理未過期的key一樣來處理過期key;
  • 從服務器只有在接到主服務器發來的del命令後,才會刪除過期key。

內存淘汰策略有哪些?

當Redis的內存超過最大允許的內存之後,Redis 會觸發內存淘汰策略,刪除一些不常用的數據,以保證Redis服務器正常運行。

  • volatile-lru:針對設置了過期時間的key,使用lru算法進行淘汰。
  • allkeys-lru:針對所有key使用lru算法進行淘汰。
  • volatile-lfu:針對設置了過期時間的key,使用lfu算法進行淘汰。
  • allkeys-lfu:針對所有key使用lfu算法進行淘汰。
  • volatile-random:針對設置了過期時間的key中使用隨機淘汰的方式進行淘汰。
  • allkeys-random:針對所有key使用隨機淘汰機制進行淘汰。
  • volatile-ttl:針對設置了過期時間的key,越早過期的越先被淘汰。

Redis對隨機淘汰和LRU策略進行的更精細化的實現,支持將淘汰目標範圍細分為全部數據和設有過期時間的數據,這種策略相對更為合理一些。因為一般設置了過期時間的數據,本身就具備可刪除性,將其直接淘汰對業務不會有邏輯上的影響;而沒有設置過期時間的數據,通常是要求常駐內存的,往往是一些配置數據或者是一些需要當做白名單含義使用的數據(比如用户信息,如果用户信息不在緩存裏,則説明用户不存在),這種如果強行將其刪除,可能會造成業務層面的一些邏輯異常。

內存淘汰策略可以通過配置文件來修改,相應的配置項是maxmemory-policy,默認配置是noeviction

緩存常見問題

Redis 中如何保證緩存與數據庫的數據一致性?

  1. 先更新緩存,再更新數據庫
  2. 先更新數據庫存,再更新緩存
  3. 先刪除緩存,再更新數據庫,後續等查詢把數據庫的數據回種到緩存中
  4. 先更新數據庫,再刪除緩存,後續等查詢把數據庫的數據回種到緩存中
  5. 緩存雙刪策略。更新數據庫之前,刪除一次緩存;更新完數據庫後,再進行一次延遲刪除
  6. 使用 Binlog 異步更新緩存,監聽數據庫的 Binlog 變化,通過異步方式更新 Redis 緩存

以上就是實現數據庫與緩存一致性的六種方式,這裏前面三種都不太推薦使用,後面三種需要根據實際場景選擇:

  • 如果是要考慮實時一致性的話,先寫 MySQL,再刪除 Redis 應該是較為優的方案,雖然短期內數據可能不一致,不過其能儘量保證數據的一致性。
  • 如果考慮最終一致性的話,推薦的是使用 binlog + 消息隊列的方式,這個方案其有重試和順序消費,能夠最大限度地保證緩存與數據庫的最終一致性:。

詳情可以看這篇文章:緩存和數據庫一致性問題

緩存雪崩

在使用緩存時,通常會對緩存設置過期時間,一方面目的是保持緩存與數據庫數據的一致性,另一方面是減少冷緩存佔用過多的內存空間。

那麼,當大量緩存數據在短時間集體失效或者 Redis 故障宕機時,請求全部轉發到數據庫,從而導致數據庫壓力驟增,甚至宕機,從而形成一系列連鎖反應,造成整個系統崩潰,這就是緩存雪崩的問題。

所以,發生緩存雪崩的場景通常有兩個:

  • 大量熱點key同時過期;
  • 緩存服務故障;

大量熱點key同時過期

針對大量數據同時過期而引發的緩存雪崩問題,常見的應對方法有下面這幾種:

  1. 均勻失效(建議):將key的過期時間後面加上一個隨機數(比如隨機1-5分鐘)

    • 如果要給緩存數據設置過期時間,應該避免將大量的數據設置成同一個過期時間。就可以給這些數據的過期時間加上一個隨機數,這樣就保證數據不會在同一時間過期。
  2. 考慮用隊列或者互斥鎖的方式,保證緩存單線程寫,但這種方案可能會影響併發量,不建議

    • 當業務線程在處理用户請求時,如果發現訪問的數據不在 Redis 裏,就加個互斥鎖,保證同一時間內只有一個請求來構建緩存(從數據庫讀取數據,再將數據更新到 Redis 裏。也就不會有大量的熱點數據需要從數據庫讀取數據了),即保證緩存單線程寫。
    • 實現互斥鎖的時候,最好設置超時時間,防止線程出現意外一直阻塞導致其它請求也無法獲取鎖。
  3. 可以讓熱點數據永久有效(不推薦,一般都會要求必須設置過期時間),後台異步更新緩存,適用於不嚴格要求緩存一致性的場景。

    • 事實上即使數據永久有效,數據也不一定會一直在內存中,這是因為 Redis的內存淘汰策略 ,當系統內存緊張的時候,有些緩存數據會被“淘汰”。如果此時用户讀取的是淘汰數據,那就有可能會返回空值,誤以為數據丟失了。解決方式:
    • 後台頻繁地檢測緩存是否有效,如果檢測到緩存失效了,那就從數據庫讀取數據,並更新到緩存。但這個頻繁 的時間不好掌握,總會有時間間隔,間隔時間內就有可能導致空值的返回;
    • 用户讀取數據時,發現數據不在Redis中,則通知後台線程更新緩存。後台線程收到消息後,發現數據不存在就讀取數據庫數據,並將數據加載到緩存。
  4. 雙key策略,主key設置過期時間,備key不設置過期時間,當主key失效時,直接返回備key值。

    • 這個只是 key 不一樣,但是 value 值是一樣的,相當於給緩存數據做了個副本。但是在更新緩存時,需要同時更新「主 key 」和「備 key 」的數據。
    • 雙 key 策略的好處是,當主 key 過期了,有大量請求獲取緩存數據的時候,直接返回備 key 的數據,這樣可以快速響應請求。而不用因為 key 失效而導致大量請求被鎖阻塞住(採用了互斥鎖,僅一個請求來構建緩存),後續再通知後台線程,重新構建主 key 的數據。
    • 但是需要同時存儲兩份數據,增大了內存開銷

      緩存服務故障

  5. 構建緩存高可用集羣(針對緩存服務故障情況)。

    • 如果 Redis 緩存的主節點故障宕機,從節點可以切換成為主節點,繼續提供緩存服務,避免了由於 Redis 故障宕機而導致的緩存雪崩問題
  6. 當緩存雪崩發生時,可以實行服務熔斷、限流、降級等措施進行保障。

    • 服務熔斷機制,就是暫停業務應用對緩存服務的訪問,直接返回錯誤,也就不用再繼續訪問數據庫,從而降低對數據庫的訪問壓力,保證數據庫系統的正常運行,然後等到 Redis 恢復正常後,再允許業務應用訪問緩存服務。這種方式就是暫停了業務訪問
    • 請求限流機制,就是隻將少部分請求發送到數據庫進行處理,再多的請求就在入口直接拒絕服務,等到 Redis 恢復正常並把緩存預熱完後,再解除請求限流的機制。

緩存擊穿

緩存雪崩是指大量熱點key同時失效的情況,如果是單個熱點key,一直都有着大併發訪問,那麼在這個key失效的瞬間,這個大併發請求就會擊破緩存,直接請求到數據庫,好像蠻力擊穿一樣。這種情況就是緩存擊穿(Cache Breakdown)。

緩存擊穿和前面提到的緩存雪崩產生的原因其實很相似。區別點在於:

  • 緩存雪崩是大面積的緩存失效導致大量請求涌入數據庫。
  • 緩存擊穿是少量緩存失效的時候恰好失效的數據遭遇大併發量的請求,導致這些請求全部涌入數據庫中。

因此可以將緩存擊穿視為緩存雪崩的子集,應對方案也是緩存雪崩説到的方案。

解決方案:

  1. 過期時間續期:比如每次請求的時候自動將過期時間續期一下
  2. 使用互斥鎖(Mutex Key):當緩存不可用時,僅持鎖的線程負責從數據庫中查詢數據並寫入緩存中,其餘請求重試時先嚐試從緩存中獲取數據,避免所有的併發請求全部同時打到數據庫上。步驟如下:

    1. 沒有命中緩存的時候,先請求獲取分佈式鎖,獲取到分佈式鎖的線程,執行DB查詢操作,然後將查詢結果寫入到緩存中;
    2. 沒有搶到分佈式鎖的請求,原地自旋等待一定時間後進行再次重試;
    3. 未搶到鎖的線程,再次重試的時候,先嚐試去緩存中獲取下是否能獲取到數據,如果可以獲取到數據,則直接取緩存已有的數據並返回;否則重複上述123步驟。
  3. 邏輯過期:熱點數據不設置過期時間,後台異步更新緩存,適用於不嚴格要求緩存一致性的場景。

對於業務中最常使用的旁路型緩存而言,通常會先讀取緩存,如果不存在則去數據庫查詢,並將查詢到的數據添加到緩存中,這樣就可以使得後面的請求繼續命中緩存。

但是這種常規操作存在個“漏洞”,因為大部分緩存容量有限制,且很多場景會基於LRU策略進行內存中熱點數據的淘汰,假如有個惡意程序(比如爬蟲)一直在刷歷史數據,容易將內存中的熱點數據變為歷史數據,導致真正的用户請求被打到數據庫層。

針對這種場景,在緩存的設計時,需要考慮到對這種冷數據的加熱機制進行一些額外處理,如設定一個門檻,如果指定時間段內對一個冷數據的訪問次數達到閾值,則將冷數據加熱,添加到熱點數據緩存中,並設定一個獨立的過期時間,來解決此類問題。

比如可以約定同一秒內對某條冷數據的請求超過10次,則將此條冷數據加熱作為臨時熱點數據存入緩存,設定緩存過期時間為30天。通過這樣的機制,來解決冷數據的突然竄熱對系統帶來的不穩定影響。

緩存穿透

緩存穿透(cache penetration)是用户訪問的數據既不在緩存當中,也不在數據庫中。出於容錯的考慮,如果從底層數據庫查詢不到數據,則不寫入緩存。這就導致每次請求都會到底層數據庫進行查詢,緩存也失去了意義。當高併發或有人利用不存在的Key頻繁攻擊時,數據庫的壓力驟增,甚至崩潰,這就是緩存穿透問題。

緩存穿透與緩存擊穿同樣非常相似,區別點在於緩存穿透的實際請求數據在數據庫中也沒有,而緩存擊穿是僅僅在緩存中沒命中,但是在數據庫中其實是存在對應數據的。

發生場景:

  • 原來數據是存在的,但由於某些原因(誤刪除、主動清理等)在緩存和數據庫層面被刪除了,但前端或前置的應用程序依舊保有這些數據;
  • 黑客惡意攻擊,外部爬蟲,故意大量訪問某些讀取不存在數據的業務;

緩存穿透解決方案:

  1. 緩存空值(null)或默認值

    • 分析業務請求,如果是正常業務請求時發生緩存穿透現象,可針對相應的業務數據,在數據庫查詢不存在時,將其緩存為空值(null)或默認值,但是需要注意的是,針對空值的緩存失效時間不宜過長,一般設置為5分鐘之內。當數據庫被寫入或更新該key的新數據時,緩存必須同時被刷新,避免數據不一致。
  2. 業務邏輯前置校驗

    • 在業務請求的入口處進行數據合法性校驗,檢查請求參數是否合理、是否包含非法值、是否惡意請求等,提前有效阻斷非法請求。比如,根據年齡查詢時,請求的年齡為-10歲,這顯然是不合法的請求參數,直接在參數校驗時進行判斷返回。
  3. 使用 布隆過濾器快速判斷數據不存在(推薦)

    • 在寫入數據時,使用布隆過濾器進行標記(相當於設置白名單),業務請求發現緩存中無對應數據時,可先通過查詢布隆過濾器判斷數據是否在白名單內(布隆過濾器可以判斷數據一定不存在),如果不在白名單內,則直接返回空或失敗。
  4. 用户黑名單限制:當發生異常情況時,實時監控訪問的對象和數據,分析用户行為,針對故意請求、爬蟲或攻擊者,進行特定用户的限制;
  5. 添加反爬策略:比如添加請求籤名校驗機制、比如添加IP訪問限制策略等等

緩存預熱

緩存預熱就是系統上線後,將相關的緩存數據直接加載到緩存系統。這樣就可以避免在用户請求的時候,先查詢數據庫,然後再將數據緩存的問題!用户直接查詢事先被預熱的緩存數據!

解決方案:

  1. 直接寫個緩存刷新頁面,上線時手工操作一下;
  2. 數據量不大,可以在項目啓動的時候自動進行加載;
  3. 定時刷新緩存;

緩存降級

當訪問量劇增、服務出現問題(如響應時間慢或不響應)或非核心服務影響到核心流程的性能時,仍然需要保證服務還是可用的,即使是有損服務。系統可以根據一些關鍵數據進行自動降級,也可以配置開關實現人工降級。

緩存降級的最終目的是保證核心服務可用,即使是有損的。而且有些服務是無法降級的(如加入購物車、結算)。

在進行降級之前要對系統進行梳理,看看系統是不是可以丟卒保帥;從而梳理出哪些必須誓死保護,哪些可降級;比如可以參考日誌級別設置預案:

  1. 一般:比如有些服務偶爾因為網絡抖動或者服務正在上線而超時,可以自動降級;
  2. 警告:有些服務在一段時間內成功率有波動(如在95~100%之間),可以自動降級或人工降級,併發送告警;
  3. 錯誤:比如可用率低於90%,或者數據庫連接池被打爆了,或者訪問量突然猛增到系統能承受的最大閥值,此時可以根據情況自動降級或者人工降級;
  4. 嚴重錯誤:比如因為特殊原因數據錯誤了,此時需要緊急人工降級。

服務降級的目的,是為了防止Redis服務故障,導致數據庫跟着一起發生雪崩問題。因此,對於不重要的緩存數據,可以採取服務降級策略,例如一個比較常見的做法就是,Redis出現問題,不去數據庫查詢,而是直接返回默認值給用户。

分佈式鎖

詳情請查看:Redis實現分佈式鎖

Redis 中如何實現分佈式鎖?

在 Redis 中實現分佈式鎖的常見方法是通過 setnx 命令 + lua 腳本組合使用。確保多個客户端不會獲得同一個資源鎖的同時,也保證了安全解鎖和意外情況下鎖的自動釋放,

LUA腳本

Redis 通過 LUA 腳本創建具有原子性的命令: 當lua腳本命令正在運行的時候,不會有其他腳本或 Redis 命令被執行,實現組合命令的原子操作。

在Redis中執行Lua腳本有兩種方法:evalevalshaeval命令使用內置的 Lua 解釋器,對 Lua 腳本進行求值。

//第一個參數是lua腳本,第二個參數是鍵名參數個數,剩下的是鍵名參數和附加參數
> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"

lua腳本作用

  1. Lua腳本在Redis中是原子執行的,執行過程中間不會插入其他命令。
  2. Lua腳本可以將多條命令一次性打包,有效地減少網絡開銷。

使用Redis + lua方式可能存在的問題?

  1. 不可重入性。同一個線程無法多次獲取同一把鎖
  2. 不可重試。獲取鎖只嘗試一次就返回false,沒有重試機制
  3. 超時釋放。鎖超時釋放雖然可以避免死鎖,但如果是業務執行耗時較長,也會導致鎖的釋放,存在安全隱患
  4. 主從一致性。如果Redis是主從集羣,主從同步存在延遲,當主機宕機時,從成為了主,但可能存在從此時還未完成同步,因此從上就沒有鎖標識,此時會出現線程安全問題。

分佈式鎖在未完成邏輯前過期怎麼辦?

若鎖在未完成邏輯前就過期,此時可能會產生數據不一致的問題,因為鎖過期了,此時如果再出現一個客户端爭搶鎖,即可拿到鎖然後同時進行業務操作,這等於鎖失效了。

此時可以在邏輯執行過程中定期續期鎖,確保鎖在處理過程中不會過期。

業界出了一個看門狗機制來防止這種情況的產生。理論很簡單,在搶到鎖之後,後台會有一個任務,定時向redis進行鎖的續期、比如鎖的過期時間是 305,可以每過三分之一時長(30/3)10s後就去redis 重新設置過期時間為 30s

在鎖被釋放的時候,就移除這個定時任務。

什麼是Reddsion?

RLock是Redisson分佈式鎖的最核心接口,繼承了concurrent包的Lock接口和自己的RLockAsync接口,RLockAsync的返回值都是RFuture,是Redisson執行異步實現的核心邏輯,也是Netty發揮的主要陣地。

redisson 是一個類庫,封裝了很多 redis 操作,便於我們使用。其實現的分佈式鎖就引入了看門狗機制,具體原理和上面所述的一致,並且 redisson 支持可重入鎖,即同一個線程可以多次獲取同一個分佈式鎖,而不會導致死鎖。

實現方法如下:在獲取鎖時,檢查當前鎖的唯一標識是否已經屬於當前線程。

  • 如果是,則增加一個重入計數器。
  • 釋放鎖時,減少重入計數器,只有當計數器為0時才真正釋放鎖。

説説 Redisson 分佈式鎖的原理?

Redison 是基於 Redis實現的分佈式鎖,實際上是使用 Redis 的原子操作來確保多線程、多進程或多節點系統中,只有一個線程能獲得鎖,避免併發操作導致的數據不一致問題

  1. 鎖的獲取:Redisson 使用Lua 腳本,利用 exists+ hexists+ hincrby 命令來保證只有一個線程能成功設置鍵(表示獲得鎖)。同時,Redisson 會通過 pexpire命令為鎖設置過期時間,防止因宕機等原因導致鎖無法釋放(即死鎖問題)。
  2. 鎖的續期:為了防止鎖在持有過程中過期導致其他線程搶佔鎖,Redisson 實現了鎖自動續期的功能(看門狗機制)。持有鎖的線程會定期續期,即更新鎖的過期時間,確保任務沒有完成時鎖不會失效。
  3. 鎖的釋放:鎖釋放時,Redisson也是通過 Lua 腳本保證釋放操作的原子性。利用 hexists+del 確保只有持有鎖的線程才能釋放鎖,防止誤釋放鎖的情況。Lua 腳本同時利用 publish 命令,廣播喚醒其它等待的線程。
  4. 可重入鎖:Redison 支持可重入鎖,持有鎖的線程可以多次獲取同一把鎖而不會被阻塞。具體是利用 Redis 中的哈希結構,哈希中的 key 為線程ID,如果重入則 value + 1,如果釋放則 value - 1 ,減到0説明鎖被釋放了,則 del 鎖。

Redisson 看門狗(watch dog)機制瞭解嗎?

Redisson 的看門狗(watchdog)主要用來避免 Redis 中的鎖在超時後業務邏輯還未執行完畢,鎖卻被自動釋放的情況。它通過定期刷新鎖的過期時間來實現自動續期

主要原理:

  1. 定時刷新:如果當前分佈式鎖未設置過期時間,Redisson基於 Netty 時間輪啓動一個定時任務,定期向 Redis 發送命令更新鎖的過期時間,默認每 10s發送一次請求,每次續期 30s.
  2. 釋放鎖:當客户端主動釋放鎖時,Redison 會取消看門狗刷新操作。如果客户端宕機了,定時任務自然也就無法執行了,此時等超時時間到了,鎖也會自動釋放。

什麼是RedLock?

Red Lock,又稱為紅鎖,是一種分佈式鎖的實現方案,旨在解決在分佈式環境中使用 Redis 實現分佈式鎖時的安全性問題。一般情況下,我們在生產環境會使用主從+哨兵方式來部署 Redis。如果我們正在使用 redis 分佈式鎖,此時發生了主從切換,但從節點上不一定已經同步了主節點的鎖信息,所以新的主節點上可能沒有鎖的信息。此時另一個業務去加鎖,一看鎖還沒被佔,於是搶到了鎖,開始執行業務邏輯。此時就發生了兩個競爭者同時進入臨界區操作臨界資源的情況,可能就會發生數據不一致的問題。所以 Redis 官方推出了紅鎖,避免這種狀況產生,它主要解決的問題就是當部分節點發生故障也不會影響鎖的使用和數據問題的產生,。

Add a new Comments

Some HTML is okay.