天天看點

聊聊Redis的資料過期、資料淘汰以及資料持久化的實作機制

作者:架構悟道
聊聊Redis的資料過期、資料淘汰以及資料持久化的實作機制

大家好,又見面了。

本文是筆者作為掘金技術社群簽約作者的身份輸出的緩存專欄系列内容,将會通過系列專題,講清楚緩存的方方面面。如果感興趣,歡迎關注以擷取後續更新。

上一篇文章中呢,我們簡單的介紹了下Redis的整體情況。作為集中式緩存的優秀代表,Redis可以幫助我們在項目中完成很多特定的功能。Redis準确的說是一個非關系型資料庫,但是由于其超高的并發處理性能,及其對于緩存場景所提供的一系列能力建構,使其成為了分布式系統中的集中緩存的絕佳選擇。

Redis對于緩存能力場景的支援,除了基礎的緩存增删改查,還支援對記錄的過期時間設定,支援多種不同的資料淘汰政策等等。此外為了解決記憶體型元件資料可靠性問題,還提供了一系列的資料持久化方案。

本篇文章中,我們就一起聊一聊這方面内容。

聊聊Redis的資料過期、資料淘汰以及資料持久化的實作機制

資料過期能力

為了節約記憶體的使用量,保證有限的記憶體空間能夠被更有價值的資料使用,是以很多記憶體緩存元件都會支援資料過期能力。之前我們提過的本地緩存元件Guava Cache、Caffeine等支援基于緩存容器對象級别設定統一的過期時間,而Redis則支援對每條記錄設定單獨的過期時間。

建立時設定過期時間

可以在建立記錄的時候指定過期時間,redis提供了setex指令可以實作插入的時候同步指定過期時間。比如:

setex key1 5 value1
           

上述指令實作了往redis中寫入一個key1記錄,并同時設定了5s後過期。如果在JAVA SpringBoot項目中可以直接使用相關API接口來實作:

stringRedisTemplate.opsForValue().set("key1", "value1", 5, TimeUnit.SECONDS);
           

這樣緩存寫入5s之後,緩存記錄就會過期失效。描述到這裡可以看出,這是一種基于建立時間來判定是否過期的機制,也即正常上說的TTL政策,當設定了過期時間之後不管有沒有被使用都會到期被強制清理掉。但有很多場景下也會期望資料能夠按照TTI(指定時間未使用再過期)的方式來過期清理,如使用者鑒權場景:

假設使用者登入系統後生成token并存儲到Redis中,指定token有效期30分鐘,那麼如果使用者一直在使用系統的時候突然時間到了然後退出要求重新登入,這個體驗感就會很差。正确的預期應該是使用者連續操作的時候就不要登出,隻有連續30分鐘沒有操作的時候才過期處理。

略有遺憾的是,Redis并不支援按照TTI機制來做資料過期處理。但是作為補償,Redis提供了一個重新設定某個key值過期時間的方法,可以通過expire方法來實作指定key的續期操作,以一種曲線救國的方式滿足訴求。

聊聊Redis的資料過期、資料淘汰以及資料持久化的實作機制

實作緩存的續期

通過expire指令,可以對已有的記錄重新設定過期時間,如果此前已經有設定了過期時間,則覆寫原先的過期時間。

expire key1 30
           

執行上述指令,可以将key1的過期時間給重新設定為30s,不管此前是否有過期時間。同樣地,在代碼中也可以友善地實作這一指令:

stringRedisTemplate.expire("key1", 30, TimeUnit.SECONDS);
           

對于上面說的使用者token續期的訴求,可以這樣來操作:

使用者首次登入成功後,會生成一個token令牌,然後将令牌與使用者資訊存儲到redis中,設定30分鐘有效期。

每次請求接口中攜帶token來鑒權,每次get請求的時候,就重新通過expire操作将token的過期時間重新設定為30分鐘。

持續30分鐘無請求後,此條token緩存資訊過期失效。

同樣實作了TTI的效果。

聊聊Redis的資料過期、資料淘汰以及資料持久化的實作機制

實作指定時刻過期

Redis的過期時間設定,是基于目前指令執行時刻開始的相對過期時間,隻能設定距離目前多久後失效,如果想要實作在固定時刻失效,還需要調用端執行一點小小的換算處理來實作。

public void test() {
    LocalDateTime dateTime = LocalDateTime.parse("2022-11-23 22:00:00", DateTimeFormatter.ofPattern("yyyy-MM-dd " +
            "HH:mm:ss"));
    Date date = Date.from(dateTime.atZone(ZoneId.systemDefault()).toInstant());
    long expireTimeLong = date.getTime() - System.currentTimeMillis();
    stringRedisTemplate.expire("key1", expireTimeLong, TimeUnit.MILLISECONDS);
}
           

通過計算出目标時刻與目前時刻的時間內插補點,作為過期時間設定到記錄上,即可。

聊聊Redis的資料過期、資料淘汰以及資料持久化的實作機制

資料淘汰政策

前面強調過,Redis是一個基于記憶體的緩存資料庫,而記憶體的容量通常是有限的。雖然Reids有提供資料過期處理邏輯,但是當資料量特别多的時候就需要資料淘汰機制來兜底了。

這裡資料淘汰政策與資料過期兩個概念的差異要先弄清楚:

  • 資料過期,是符合業務預期的一種資料删除機制,為記錄設定過期時間,過期後從緩存中移除。
  • 資料淘汰,是一種“有損自保”的降級政策,是業務預期之外的一種資料删除手段。指的是所存儲的資料沒達到過期時間,但緩存空間滿了,對于新的資料想要加入記憶體時,為了避免OOM而需要執行的一種應對政策。

試想下,把Redis當做一個容器,容器已滿的情況下繼續往裡面放東西,應對之法其實就兩種:

  1. 直接拒絕放入。
  2. 扔掉容器中部分已有内容,騰出空間接納新内容放入。
聊聊Redis的資料過期、資料淘汰以及資料持久化的實作機制

遵循上述認知,Redis提供了6種不同的資料淘汰機制,供使用方按需選擇,将有限的空間僅用來存儲熱點資料,實作緩存的價值最大化。如下:

聊聊Redis的資料過期、資料淘汰以及資料持久化的實作機制

對幾種政策具體含義梳理歸納如下表所示:

資料淘汰政策 具體含義說明
noeviction 淘汰新進入的資料,即拒絕新内容寫入緩存,直到緩存有新的空間。
allkeys-lru 将記憶體中已有的key内容按照LRU政策将最久沒有使用的記錄淘汰掉,然後騰出空間用來存放新的記錄。
volatile-lru 從設定了過期時間的key裡面按照LRU政策,淘汰掉最久沒有使用的記錄。與allkeys-lru相比,這種方式僅會在設定了過期時間的key裡面進行淘汰。
allkeys-random 從已有的所有key裡面随機剔除部分,騰出空間容納新資料。
volatile-random 從已有的設定了過期時間的key裡面随機剔除部分,騰出空間容納新的資料
volatile-ttl 從已有的設定了過期時間的key裡面,将最近将要過期的資料提前剔除掉,與volatile-lru的差別在于排序邏輯不一樣,一個基于ttl規則排序,一個基于lru政策排序。

從上述政策裡面可以看出,根據LRU和Random兩種操作的範圍不同,各自又細分了兩種不同的執行政策。

  • 從設定過期時間的key裡進行淘汰

相對來說,設定了過期時間的資料,說明業務層面已經默許了其可以被删除,是以即使被提前淘汰了,對業務層面的影響也是比較小的。

系統中緩存最近30分鐘的使用者浏覽曆史記錄,即使這些資料被删除淘汰,對系統主體功能而言,不會受損。
  • 從全量key裡面執行淘汰

從全量資料裡面執行淘汰,就有可能淘汰掉沒有設定過期時間的key記錄。未設定過期時間的資料如果資料被淘汰掉,很有可能會影響業務的運作邏輯邏輯正确性。

緩存中存儲了系統内的黑名單使用者清單,使用者鑒權的時候,會判斷使用者是否在黑名單中,如果在黑名單中則禁止登入。這個黑名單是永久的,不會自己解封。如果由于被動淘汰政策觸發删除部分黑名單,那原先的黑名單使用者就會不受限制而進入到系統中,導緻預期之外的情況發生。

不得不說,Redis的這一細分處理原則,還是很貼心的。具體實踐中,可以根據自身系統記憶體儲的資料體量以及存儲的資料内容性質,選擇合适的資料淘汰政策。

聊聊Redis的資料過期、資料淘汰以及資料持久化的實作機制

資料持久化方案

除了容量有限之外,存儲在記憶體中的資料最大的風險點是什麼?資料丢失!

因為記憶體中的資料是非持久化存儲的,一旦斷電或者出現系統異常等情況,很容易導緻記憶體資料丢失。是以大部分的系統裡面都隻是将記憶體型緩存用作資料庫的輔助扛壓,最終的資料存儲在DB等可以持久化存儲容器中,同步一份資料到緩存中用于并發場景下的業務使用。

聊聊Redis的資料過期、資料淘汰以及資料持久化的實作機制

這種組網場景下,Redis的資料其實是沒有持久化的訴求的,因為Redis中資料僅僅是一份副本,最終資料在DB中都有。即使系統異常或者掉電重新開機,也可以基于資料庫的資料進行緩存重建 —— 最多就是資料量特别巨大的時候,重建緩存的耗時會比較長。

另外一種場景,業務裡面會有有些寫操作會比較頻繁、強依賴Redis特性來實作的功能,這部分資料不能丢、但又沒有重要到必須每次更新都需要存入DB的地步。比如部落格系統中的文章閱讀量資料,文章每次被讀取都需要更新閱讀數,寫操作非常頻繁,如果閱讀量存儲到DB中,會導緻DB壓力較大,這種情況就希望可以将資料存儲在記憶體中,然後記憶體資料可以持久化儲存。

聊聊Redis的資料過期、資料淘汰以及資料持久化的實作機制

Redis提供了多種持久化方案,可以實作将記憶體資料定期存儲到磁盤上,重新開機時候可以從磁盤加載到記憶體中,以此來避免資料的丢失。

下面一起看下。

RDB全量持久化模式

全量模式很好了解,就是定時将目前記憶體裡面所有的key-value鍵值對内容,全部導出一份快照資料存儲到磁盤上。這樣下次如果需要使用的時候,就可以從磁盤上加載快照檔案,實作記憶體資料的恢複。

RDB全量模式持久化将資料寫入磁盤的動作可以分為SAVE與BGSAVE兩種。所謂BGSAVE就是background-save,也就是背景異步save,差別點在于SAVE是由Redis的指令執行線程按照普通指令的方式去執行操作,而BGSAVE是通過fork出一個新的程序,在新的獨立程序裡面去執行save操作。

聊聊Redis的資料過期、資料淘汰以及資料持久化的實作機制

還記得前面文章中說的麼?Redis的請求指令執行是通過單線程的方式執行的,是以要盡量避免耗時操作,而save動作需要将記憶體全部資料寫入到磁盤上,對于redis而言,這一操作是非常耗時的,會阻塞住全部正常業務請求,是以save操作的觸發隻有兩個場景:

  1. 用戶端手動發送save指令執行
  2. Redis在shutdown的時候自動執行

從資料儲存完備性方面看,這兩種方式都起不到自動持久化備份的能力,如果出現一些機器掉電等情況,是不會觸發redis shutdown操作的,将面臨資料丢失的風險。

相比而言,bgsave的殺傷力要小一些、适用度也更好一些,它可以保證在持久化期間Redis主程序可以繼續處理業務請求。bgsave增加了過程中自動持久化操作的機制,觸發條件更加的“智能”:

  1. 用戶端手動指令觸發bgsave操作
  2. Redis配置定時任務觸發(支援間隔時間+變更資料量雙重次元綜合判斷,達到任一條件則觸發)

此外,在master-slave主從部署的場景中還支援僅由slave節點觸發bgsave操作,來降低對master節點的影響。值得注意的是,在fork子程序的時候需要将redis主程序中記憶體所有資料都複制一份到子程序中,是以bgsave操作實際上是将子程序記憶體中的資料快照導出到磁盤上,在執行期間對機器的剩餘記憶體有較高要求,如果機器剩餘記憶體不足,則可能導緻fork的時候兩份記憶體資料量超過機器實體記憶體大小,導緻系統啟用虛拟記憶體,拷貝速度大打折扣(虛拟記憶體本質上就是把磁盤當記憶體用,操作速度相比實體記憶體大大降低),會阻塞住Redis主程序的指令執行。

聊聊Redis的資料過期、資料淘汰以及資料持久化的實作機制

如果開啟了RDB的bgsave定時觸發執行機制,在出現異常掉電等情況,可能會丢失最後一部分尚未來及持久化的内容。在恢複的時候,Redis啟動之後會先去讀取RDB檔案然後将其寫入記憶體中恢複此前的緩存資料,資料恢複期間不受理外部業務請求。

聊聊Redis的資料過期、資料淘汰以及資料持久化的實作機制

AOF增量同步方式

RDB全量模式簡單粗暴,直接将記憶體全量資料存儲為快照序列化到本地。AOF(Append Only File)與RDB的思路不同,AOF更像是記錄住Redis的每一次寫請求執行指令,将每次執行的寫操作指令記錄存儲到磁盤上,然後通過一種類似指令重放執行的方式,來實作資料的恢複。

聊聊Redis的資料過期、資料淘汰以及資料持久化的實作機制

AOF具體實作的時候,包含幾種不同的政策:

  • always

可以簡單的了解為每一條redis寫請求執行的時候會觸發一次磁盤寫入操作,且隻有在磁盤寫入完成之後,請求的響應才會傳回。這種方式可以保證AOF記錄的準确性,但是會嚴重影響Redis的并發吞吐量。

  • every sec

異步執行,任務執行線程執行指令後将指令寫入任務放入隊列中,由子線程異步方式每秒一次将執行指令分批寫入檔案中,相比always方式在異常情況下可能會丢失最後1s的執行記錄,但可以大大降低對redis指令執行效率的影響。

  • no

redis不控制落盤時間,由作業系統去決定什麼時候該往磁盤flush,這種情況一般不推薦使用,無法準确掌控是否落盤,可靠性不夠。

AOF的方式落盤持久化的時候,每次僅寫入增量的部分,是以對系統整體運作期的影響較小,但随着系統線上運作時長的累加,AOF中存儲的指令也越來越多,這樣問題也随着出現:

  1. AOF寫入的方式類似與日志列印,将請求追加寫入到磁盤檔案中,文本檔案未經過壓縮,時間久了之後會占據大量磁盤空間,易造成磁盤滿的問題。
  2. 在需要從AOF檔案回放重新建構緩存内容時,可能會耗時較久(相當于要将長期累積下來的寫操作指令逐個重新執行一下)。
聊聊Redis的資料過期、資料淘汰以及資料持久化的實作機制

RDB與AOF混合使用

從前面的介紹中可以看出:

  • RDB在過程中每次寫磁盤的時候對Redis業務處理的性能影響較大,但是從磁盤加載到記憶體重建緩存的時候效率很高。
  • AOF通過增量的方式降低了運作過程中對Redis業務處理的影響,但是指令回放重建緩存的時候效率較差。

如果将兩者結合起來使用,是否可以取長補短呢?事實似乎的确如此。從4.0版本開始,Redis支援了RDB + AOF的混合持久化方式,通過rewrite機制來實作。需要在redis的配置檔案中開啟對應開關:

aof-use-rdb-preamble yes
           

開啟之後,redis在每次執行aof操作的時候會判斷下是否達到了觸發rewrite的條件,如果達到,則fork出一個新的子程序進行RDB操作将目前時刻全量記憶體資料生成RDB資料然後寫入到AOF檔案中,而後續的寫操作指令則繼續append方式追加記錄到AOF檔案中。這樣一來AOF檔案實際上由兩部分内容組成。如下圖所示:

聊聊Redis的資料過期、資料淘汰以及資料持久化的實作機制

通過RDB + AOF混合的政策,很好的實作了兩者的優勢互補:

  1. 先通過AOF的方式記錄指令,達到門檻的時候才執行rewrite操作生成RDB,最大限度降低了RDB執行頻率,降低了對redis業務指令處理過程的影響。
  2. 通過RDB的方式替代了前期大量的AOF指令存儲,有效的降低了磁盤占用。
  3. 通過RDB + AOF的方式,系統重建緩存的時候,先加載RDB檔案完成主體資料的重建,然後在此基礎上重放AOF增量指令,大大降低了啟動時AOF重放的耗時。
聊聊Redis的資料過期、資料淘汰以及資料持久化的實作機制

小結回顧

好啦,關于Redis的資料過期設定、資料淘汰機制以及資料持久化政策等方面的問題,就讨論到這裡了。那麼你對Redis是否有了新的了解呢?你覺得Redis的哪個方面特性最打動了你呢?歡迎評論區一起交流下,期待和各位小夥伴們一起切磋、共同成長。

補充說明 :

本文屬于《深入了解緩存原理與實戰設計》系列專欄的内容之一。該專欄圍繞緩存這個宏大命題進行展開闡述,全方位、系統性地深度剖析各種緩存實作政策與原理、以及緩存的各種用法、各種問題應對政策,并一起探讨下緩存設計的哲學。

如果有興趣,也歡迎關注此專欄。

聊聊Redis的資料過期、資料淘汰以及資料持久化的實作機制

我是悟道,聊技術、又不僅僅聊技術~

如果覺得有用,請點贊 + 關注讓我感受到您的支援。也可以關注下我的公衆号【架構悟道】,擷取更及時的更新。

期待與你一起探讨,一起成長為更好的自己。

聊聊Redis的資料過期、資料淘汰以及資料持久化的實作機制

繼續閱讀