Redis筆記-內存優化(一)


一、正確使用redis 數據類型

  我們先了解下 String 類型的內存空間消耗問題,以及選擇節省內存開銷的數據類型的解決方案。例如一個圖片存儲系統,要求這個系統能快速地記錄圖片 ID 和圖片在存儲系統中保存時的 ID(可以直接叫作圖片存儲對象 ID)。同時,還要能夠根據圖片 ID 快速查找到圖片存儲對象 ID。因為圖片數量巨大,所以我們就用 10 位數來表示圖片 ID 和圖片存儲對象 ID,例如,圖片 ID 為 1101000051,它在存儲系統中對應的 ID 號是 3301000051。

photo_id: 1101000051
photo_obj_id: 3301000051

如果我們的第一個方案就是用 String 保存數據。我們把圖片 ID 和圖片存儲對象 ID 分別作為鍵值對的 key 和 value 來保存,其中,圖片存儲對象 ID 用了 String 類型。剛開始,我們保存了 1 億張圖片,大約用了 6.4GB 的內存。但是,隨着圖片數據量的不斷增加,我們的 Redis 內存使用量也在增加,結果就遇到了大內存 Redis 實例因為生成 RDB 而響應變慢的問題。很顯然,String 類型並不是一種好的選擇,我們還需要進一步尋找能節省內存開銷的數據類型方案。在這個過程中,發現String類型的內存開銷巨大,對“萬金油”的 String 類型有了全新的認知:String 類型並不是適用於所有場合的,它有一個明顯的短板,就是它保存數據時所消耗的內存空間較多。

為什么String類型內存開銷大呢?

在剛才的案例中,我們保存了 1 億張圖片的信息,用了約 6.4GB 的內存,一個圖片 ID 和圖片存儲對象 ID 的記錄平均用了 64 字節。但問題是,一組圖片 ID 及其存儲對象 ID 的記錄,實際只需要 16 字節就可以了。

我們來分析一下。圖片 ID 和圖片存儲對象 ID 都是 10 位數,我們可以用兩個 8 字節的 Long 類型表示這兩個 ID。因為 8 字節的 Long 類型最大可以表示 2 的 64 次方的數值,所以肯定可以表示 10 位數。但是,為什么 String 類型卻用了 64 字節呢?

其實,除了記錄實際數據,String 類型還需要額外的內存空間記錄數據長度、空間使用等信息,這些信息也叫作元數據。當實際保存的數據較小時,元數據的空間開銷就顯得比較大了。那么,String 類型具體是怎么保存數據的呢?

因為當你保存 64 位有符號整數時,String 類型會把它保存為一個 8 字節的 Long 類型整數,這種保存方式通常也叫作 int 編碼方式。但是,當你保存的數據中包含字符時,String 類型就會用簡單動態字符串(Simple Dynamic String,SDS)結構體來保存,如下圖所示:

 

 

 

 

 

buf:字節數組,保存實際數據。為了表示字節數組的結束,Redis 會自動在數組最后加一個“\0”,這就會額外占用 1 個字節的開銷。

len:占 4 個字節,表示 buf 的已用長度。

alloc:也占個 4 字節,表示 buf 的實際分配長度,一般大於 len。

可以看到,在 SDS 中,buf 保存實際數據,而 len 和 alloc 本身其實是 SDS 結構體的額外開銷。另外,對於 String 類型來說,除了 SDS 的額外開銷,還有一個來自於 RedisObject 結構體的開銷。因為 Redis 的數據類型有很多,而且,不同數據類型都有些相同的元數據要記錄(比如最后一次訪問的時間、被引用的次數等),所以,Redis 會用一個 RedisObject 結構體來統一記錄這些元數據,同時指向實際數據。一個 RedisObject 包含了 8 字節的元數據和一個 8 字節指針,這個指針再進一步指向具體數據類型的實際數據所在,例如指向 String 類型的 SDS 結構所在的內存地址,可以看一下下面的示意圖。

 

 

 

 

 為了節省內存空間,Redis 還對 Long 類型整數和 SDS 的內存布局做了專門的設計。一方面,當保存的是 Long 類型整數時,RedisObject 中的指針就直接賦值為整數數據了,這樣就不用額外的指針再指向整數了,節省了指針的空間開銷。另一方面,當保存的是字符串數據,並且字符串小於等於 44 字節時,RedisObject 中的元數據、指針和 SDS 是一塊連續的內存區域,這樣就可以避免內存碎片。這種布局方式也被稱為 embstr 編碼方式。當然,當字符串大於 44 字節時,SDS 的數據量就開始變多了,Redis 就不再把 SDS 和 RedisObject 布局在一起了,而是會給 SDS 分配獨立的空間,並用指針指向 SDS 結構。這種布局方式被稱為 raw 編碼模式。int、embstr 和 raw 這三種編碼模式示意圖如下:

 好了,知道了 RedisObject 所包含的額外元數據開銷,借來下來計算 String 類型的內存使用量了。

因為 10 位數的圖片 ID 和圖片存儲對象 ID 是 Long 類型整數,所以可以直接用 int 編碼的 RedisObject 保存。每個 int 編碼的 RedisObject 元數據部分占 8 字節,指針部分被直接賦值為 8 字節的整數了。此時,每個 ID 會使用 16 字節,加起來一共是 32 字節。但是,另外的 32 字節去哪兒了呢?

如果大家有了解過redis的底層數據結構的話,Redis 會使用一個全局哈希表保存所有鍵值對,哈希表的每一項是一個 dictEntry 的結構體,用來指向一個鍵值對。dictEntry 結構中有三個 8 字節的指針,分別指向 key、value 以及下一個 dictEntry,三個指針共 24 字節,如下圖所示:

 

 

 但是,這三個指針只有 24 字節,為什么會占用了 32 字節呢?這就要提到 Redis 使用的內存分配庫 jemalloc 了。jemalloc 在分配內存時,會根據我們申請的字節數 N,找一個比 N 大,但是最接近 N 的 2 的冪次數作為分配的空間,這樣可以減少頻繁分配的次數。

舉個例子。如果你申請 6 字節空間,jemalloc 實際會分配 8 字節空間;如果你申請 24 字節空間,jemalloc 則會分配 32 字節。所以,在我們剛剛說的場景里,dictEntry 結構就占用了 32 字節。

那么我們該選擇redis那種數據結構呢?

Redis 有一種底層數據結構,叫壓縮列表(ziplist),這是一種非常節省內存的結構。壓縮列表的構成由三個字段 zlbytes、zltail 和 zllen,分別表示列表長度、列表尾的偏移量,以及列表中的 entry 個數。壓縮列表尾還有一個 zlend,表示列表結束。

 

 

 壓縮列表之所以能節省內存,就在於它是用一系列連續的 entry 保存數據。每個 entry 的元數據包括下面幾部分:

prev_len,表示前一個 entry 的長度。prev_len 有兩種取值情況:1 字節或 5 字節。取值 1 字節時,表示上一個 entry 的長度小於 254 字節。雖然 1 字節的值能表示的數值范圍是 0 到 255,但是壓縮列表中 zlend 的取值默認是 255,因此,就默認用 255 表示整個壓縮列表的結束,其他表示長度的地方就不能再用 255 這個值了。所以,當上一個 entry 長度小於 254 字節時,prev_len 取值為 1 字節,否則,就取值為 5 字節。

len:表示自身長度,4 字節;

encoding:表示編碼方式,1 字節;

content:保存實際數據。

 

這些 entry 會挨個兒放置在內存中,不需要再用額外的指針進行連接,這樣就可以節省指針所占用的空間。我們以保存圖片存儲對象 ID 為例,來分析一下壓縮列表是如何節省內存空間的。每個 entry 保存一個圖片存儲對象 ID(8 字節),此時,每個 entry 的 prev_len 只需要 1 個字節就行,因為每個 entry 的前一個 entry 長度都只有 8 字節,小於 254 字節。這樣一來,一個圖片的存儲對象 ID 所占用的內存大小是 14 字節(1+4+1+8=14),實際分配 16 字節。

Redis 基於壓縮列表實現了 List、Hash 和 Sorted Set 這樣的集合類型,這樣做的最大好處就是節省了 dictEntry 的開銷。當你用 String 類型時,一個鍵值對就有一個 dictEntry,要用 32 字節空間。但采用集合類型時,一個 key 就對應一個集合的數據,能保存的數據多了很多,但也只用了一個 dictEntry,這樣就節省了內存。這個方案聽起來很好,但還存在一個問題:在用集合類型保存鍵值對時,一個鍵對應了一個集合的數據,但是在我們的場景中,一個圖片 ID 只對應一個圖片的存儲對象 ID,我們該怎么用集合類型呢?換句話說,在一個鍵對應一個值(也就是單值鍵值對)的情況下,我們該怎么用集合類型來保存這種單值鍵值對呢?

在保存單值的鍵值對時,可以采用基於 Hash 類型的二級編碼方法。這里說的二級編碼,就是把一個單值的數據拆分成兩部分,前一部分作為 Hash 集合的 key,后一部分作為 Hash 集合的 value,這樣一來,我們就可以把單值數據保存到 Hash 集合中了。以圖片 ID 1101000060 和圖片存儲對象 ID 3302000080 為例,我們可以把圖片 ID 的前 7 位(1101000)作為 Hash 類型的鍵,把圖片 ID 的最后 3 位(060)和圖片存儲對象 ID 分別作為 Hash 類型值中的 key 和 value。按照這種設計方法,我在 Redis 中插入了一組圖片 ID 及其存儲對象 ID 的記錄,並且用 info 命令查看了內存開銷,我發現,增加一條記錄后,內存占用只增加了 16 字節,如下所示:

127.0.0.1:6379> info memory
#Memory
used_memory:1039120
127.0.0.1:6379> hset 1101000 060 3302000080
(integer) 1
127.0.0.1:6379> info memory
# Memory
used_memory:1039136

在使用 String 類型時,每個記錄需要消耗 64 字節,這種方式卻只用了 16 字節,所使用的內存空間是原來的 1/4,滿足了我們節省內存空間的需求。不過,你可能也會有疑惑:“二級編碼一定要把圖片 ID 的前 7 位作為 Hash 類型的鍵,把最后 3 位作為 Hash 類型值中的 key 嗎?”其實,二級編碼方法中采用的 ID 長度是有講究的。

Redis Hash 類型的兩種底層實現結構,分別是壓縮列表和哈希表。那么,Hash 類型底層結構什么時候使用壓縮列表,什么時候使用哈希表呢?

其實,Hash 類型設置了用壓縮列表保存數據時的兩個閾值,一旦超過了閾值,Hash 類型就會用哈希表來保存數據了。這兩個閾值分別對應以下兩個配置項:

hash-max-ziplist-entries:表示用壓縮列表保存時哈希集合中的最大元素個數。

hash-max-ziplist-value:表示用壓縮列表保存時哈希集合中單個元素的最大長度。

如果我們往 Hash 集合中寫入的元素個數超過了 hash-max-ziplist-entries,或者寫入的單個元素大小超過了 hash-max-ziplist-value,Redis 就會自動把 Hash 類型的實現結構由壓縮列表轉為哈希表。一旦從壓縮列表轉為了哈希表,Hash 類型就會一直用哈希表進行保存,而不會再轉回壓縮列表了。在節省內存空間方面,哈希表就沒有壓縮列表那么高效了。

為了能充分使用壓縮列表的精簡內存布局,我們一般要控制保存在 Hash 集合中的元素個數。所以,在剛才的二級編碼中,我們只用圖片 ID 最后 3 位作為 Hash 集合的 key,也就保證了 Hash 集合的元素個數不超過 1000,同時,我們把 hash-max-ziplist-entries 設置為 1000,這樣一來,Hash 集合就可以一直使用壓縮列表來節省內存空間了。

 

二、內存碎片化

 在使用 Redis 時,我們經常會遇到這樣一個問題:明明做了數據刪除,數據量已經不大了,為什么使用 top 命令查看時,還會發現 Redis 占用了很多內存呢?實際上,這是因為,當數據刪除后,Redis 釋放的內存空間會由內存分配器管理,並不會立即返回給操作系統。所以,操作系統仍然會記錄着給 Redis 分配了大量內存。但是,這往往會伴隨一個潛在的風險點:Redis 釋放的內存空間可能並不是連續的,那么,這些不連續的內存空間很有可能處於一種閑置的狀態。這就會導致一個問題:雖然有空閑空間,Redis 卻無法用來保存數據,不僅會減少 Redis 能夠實際保存的數據量,還會降低 Redis 運行機器的成本回報率。

Redis 中的內存碎片是什么原因導致的呢?接下來,我帶你來具體看一看。我們只有了解了內存碎片的成因,才能對症下葯,把 Redis 占用的內存空間充分利用起來,增加存儲的數據量。

其實,內存碎片的形成有內因和外因兩個層面的原因。簡單來說,內因是操作系統的內存分配機制,外因是 Redis 的負載特征。

內因:內存分配器的分配策略

內存分配器的分配策略就決定了操作系統無法做到“按需分配”。這是因為,內存分配器一般是按固定大小來分配內存,而不是完全按照應用程序申請的內存空間大小給程序分配。

Redis 可以使用 libc、jemalloc、tcmalloc 多種內存分配器來分配內存,默認使用 jemalloc。接下來,我就以 jemalloc 為例,來具體解釋一下。其他分配器也存在類似的問題。jemalloc 的分配策略之一,是按照一系列固定的大小划分內存空間,例如 8 字節、16 字節、32 字節、48 字節,…, 2KB、4KB、8KB 等。當程序申請的內存最接近某個固定值時,jemalloc 會給它分配相應大小的空間。

這樣的分配方式本身是為了減少分配次數。例如,Redis 申請一個 20 字節的空間保存數據,jemalloc 就會分配 32 字節,此時,如果應用還要寫入 10 字節的數據,Redis 就不用再向操作系統申請空間了,因為剛才分配的 32 字節已經夠用了,這就避免了一次分配操作。但是,如果 Redis 每次向分配器申請的內存空間大小不一樣,這種分配方式就會有形成碎片的風險,而這正好來源於 Redis 的外因了。

外因:鍵值對大小不一樣和刪改操作

Redis 通常作為共用的緩存系統或鍵值數據庫對外提供服務,所以,不同業務應用的數據都可能保存在 Redis 中,這就會帶來不同大小的鍵值對。這樣一來,Redis 申請內存空間分配時,本身就會有大小不一的空間需求。這是第一個外因。

從上面,我們知道內存分配器只能按固定大小分配內存,所以,分配的內存空間一般都會比申請的空間大一些,不會完全一致,這本身就會造成一定的碎片,降低內存空間存儲效率。

比如說,應用 A 保存 6 字節數據,jemalloc 按分配策略分配 8 字節。如果應用 A 不再保存新數據,那么,這里多出來的 2 字節空間就是內存碎片了,如下圖所示:

 

 

 第二個外因是,這些鍵值對會被修改和刪除,這會導致空間的擴容和釋放。具體來說,一方面,如果修改后的鍵值對變大或變小了,就需要占用額外的空間或者釋放不用的空間。另一方面,刪除的鍵值對就不再需要內存空間了,此時,就會把空間釋放出來,形成空閑空間。

如下圖:

 

 

 一開始,應用 A、B、C、D 分別保存了 3、1、2、4 字節的數據,並占據了相應的內存空間。然后,應用 D 刪除了 1 個字節,這個 1 字節的內存空間就空出來了。緊接着,應用 A 修改了數據,從 3 字節變成了 4 字節。為了保持 A 數據的空間連續性,操作系統就需要把 B 的數據拷貝到別的空間,比如拷貝到 D 剛剛釋放的空間中。此時,應用 C 和 D 也分別刪除了 2 字節和 1 字節的數據,整個內存空間上就分別出現了 2 字節和 1 字節的空閑碎片。如果應用 E 想要一個 3 字節的連續空間,顯然是不能得到滿足的。因為,雖然空間總量夠,但卻是碎片空間,並不是連續的。

好了,到這里,我們就知道了造成內存碎片的內外因素,其中,內存分配器策略是內因,而 Redis 的負載屬於外因,包括了大小不一的鍵值對和鍵值對修改刪除帶來的內存空間變化。大量內存碎片的存在,會造成 Redis 的內存實際利用率變低,接下來,我們就要來解決這個問題了。不過,在解決問題前,我們要先判斷 Redis 運行過程中是否存在內存碎片。

如何判斷是否有內存碎片?

Redis 是內存數據庫,內存利用率的高低直接關系到 Redis 運行效率的高低。為了讓用戶能監控到實時的內存使用情況,Redis 自身提供了 INFO 命令,可以用來查詢內存使用的詳細信息,命令如下:

INFO memory
# Memory
used_memory:1073741736
used_memory_human:1024.00M
used_memory_rss:1997159792
used_memory_rss_human:1.86G
…
mem_fragmentation_ratio:1.86

這里有一個 mem_fragmentation_ratio 的指標,它表示的就是 Redis 當前的內存碎片率。那么,這個碎片率是怎么計算的呢?其實,就是上面的命令中的兩個指標 used_memory_rss 和 used_memory 相除的結果。

mem_fragmentation_ratio = used_memory_rss/ used_memory

used_memory_rss 是操作系統實際分配給 Redis 的物理內存空間,里面就包含了碎片;而 used_memory 是 Redis 為了保存數據實際申請使用的空間。我簡單舉個例子。例如,Redis 申請使用了 100 字節(used_memory),操作系統實際分配了 128 字節(used_memory_rss),此時,mem_fragmentation_ratio 就是 1.28。

那么,知道了這個指標,我們該如何使用呢?

在這兒,我提供一些經驗閾值:

    mem_fragmentation_ratio 大於 1 但小於 1.5。這種情況是合理的。這是因為,剛才我介紹的那些因素是難以避免的。畢竟,內因的內存分配器是一定要使用的,分配策略都是通用的,不會輕易修改;而外因由 Redis 負載決定,也無法限制。所以,存在內存碎片也是正常的。

    mem_fragmentation_ratio 大於 1.5 。這表明內存碎片率已經超過了 50%。一般情況下,這個時候,我們就需要采取一些措施來降低內存碎片率了。

如何清理內存碎片?

當 Redis 發生內存碎片后,一個“簡單粗暴”的方法就是重啟 Redis 實例。當然,這並不是一個“優雅”的方法,畢竟,重啟 Redis 會帶來兩個后果:

    如果 Redis 中的數據沒有持久化,那么,數據就會丟失;

    即使 Redis 數據持久化了,我們還需要通過 AOF 或 RDB 進行恢復,恢復時長取決於 AOF 或 RDB 的大小,如果只有一個 Redis 實例,恢復階段無法提供服務。

幸運的是,從 4.0-RC3 版本以后,Redis 自身提供了一種內存碎片自動清理的方法,我們先來看這個方法的基本機制。內存碎片清理,簡單來說,就是“搬家讓位,合並空間”。

不過,需要注意的是:

碎片清理是有代價的,操作系統需要把多份數據拷貝到新位置,把原有空間釋放出來,這會帶來時間開銷。因為 Redis 是單線程,在數據拷貝時,Redis 只能等着,這就導致 Redis 無法及時處理請求,性能就會降低。而且,有的時候,數據拷貝還需要注意順序,就像剛剛說的清理內存碎片的例子,操作系統需要先拷貝 D,並釋放 D 的空間后,才能拷貝 B。這種對順序性的要求,會進一步增加 Redis 的等待時間,導致性能降低。

那么,有什么辦法可以盡量緩解這個問題嗎?

Redis 專門為自動內存碎片清理功機制設置的參數了。我們可以通過設置參數,來控制碎片清理的開始和結束時機,以及占用的 CPU 比例,從而減少碎片清理對 Redis 本身請求處理的性能影響。首先,Redis 需要啟用自動內存碎片清理,可以把 activedefrag 配置項設置為 yes,命令如下:

config set activedefrag yes

這個命令只是啟用了自動清理功能,但是,具體什么時候清理,會受到下面這兩個參數的控制。這兩個參數分別設置了觸發內存清理的一個條件,如果同時滿足這兩個條件,就開始清理。在清理的過程中,只要有一個條件不滿足了,就停止自動清理。

     active-defrag-ignore-bytes 100mb:表示內存碎片的字節數達到 100MB 時,開始清理;

     active-defrag-threshold-lower 10:表示內存碎片空間占操作系統分配給 Redis 的總空間比例達到 10% 時,開始清理。

為了盡可能減少碎片清理對 Redis 正常請求處理的影響,自動內存碎片清理功能在執行時,還會監控清理操作占用的 CPU 時間,而且還設置了兩個參數,分別用於控制清理操作占用的 CPU 時間比例的上、下限,既保證清理工作能正常進行,又避免了降低 Redis 性能。這兩個參數具體如下:

       active-defrag-cycle-min 25: 表示自動清理過程所用 CPU 時間的比例不低於 25%,保證清理能正常開展;

       active-defrag-cycle-max 75:表示自動清理過程所用 CPU 時間的比例不高於 75%,一旦超過,就停止清理,從而避免在清理時,大量的內存拷貝阻塞 Redis,導致響應延遲升高。

自動內存碎片清理機制在控制碎片清理啟停的時機上,既考慮了碎片的空間占比、對 Redis 內存使用效率的影響,還考慮了清理機制本身的 CPU 時間占比、對 Redis 性能的影響。而且,清理機制還提供了 4 個參數,讓我們可以根據實際應用中的數據量需求和性能要求靈活使用,建議你在實踐中好好地把這個機制用起來。

 

合理使用Redis 緩存有淘汰策略

Redis 4.0 之前一共實現了 6 種內存淘汰策略,在 4.0 之后,又增加了 2 種策略。我們可以按照是否會進行數據淘汰把它們分成兩類:

       不進行數據淘汰的策略,只有 noeviction 這一種。

       會進行淘汰的 7 種其他策略。

 

會進行淘汰的 7 種策略,我們可以再進一步根據淘汰候選數據集的范圍把它們分成兩類:

       在設置了過期時間的數據中進行淘汰,包括 volatile-random、volatile-ttl、volatile-lru、volatile-lfu(Redis 4.0 后新增)四種。

       在所有數據范圍內進行淘汰,包括 allkeys-lru、allkeys-random、allkeys-lfu(Redis 4.0 后新增)三種。

 

 

 默認情況下,Redis 在使用的內存空間超過 maxmemory 值時,並不會淘汰數據,也就是設定的 noeviction 策略。對應到 Redis 緩存,也就是指,一旦緩存被寫滿了,再有寫請求來時,Redis 不再提供服務,而是直接返回錯誤。Redis 用作緩存時,實際的數據集通常都是大於緩存容量的,總會有新的數據要寫入緩存,這個策略本身不淘汰數據,也就不會騰出新的緩存空間,我們不把它用在 Redis 緩存中。

volatile-random、volatile-ttl、volatile-lru 和 volatile-lfu 這四種淘汰策略。它們篩選的候選數據范圍,被限制在已經設置了過期時間的鍵值對上。也正因為此,即使緩存沒有寫滿,這些數據如果過期了,也會被刪除。

例如,我們使用 EXPIRE 命令對一批鍵值對設置了過期時間后,無論是這些鍵值對的過期時間是快到了,還是 Redis 的內存使用量達到了 maxmemory 閾值,Redis 都會進一步按照 volatile-ttl、volatile-random、volatile-lru、volatile-lfu 這四種策略的具體篩選規則進行淘汰。

    volatile-ttl 在篩選時,會針對設置了過期時間的鍵值對,根據過期時間的先后進行刪除,越早過期的越先被刪除。

    volatile-random 就像它的名稱一樣,在設置了過期時間的鍵值對中,進行隨機刪除。

    volatile-lru 會使用 LRU 算法篩選設置了過期時間的鍵值對。

    volatile-lfu 會使用 LFU 算法選擇設置了過期時間的鍵值對。

 

相對於 volatile-ttl、volatile-random、volatile-lru、volatile-lfu 這四種策略淘汰的是設置了過期時間的數據,allkeys-lru、allkeys-random、allkeys-lfu 這三種淘汰策略的備選淘汰數據范圍,就擴大到了所有鍵值對,無論這些鍵值對是否設置了過期時間。它們篩選數據進行淘汰的規則是:

  allkeys-random 策略,從所有鍵值對中隨機選擇並刪除數據;

  allkeys-lru 策略,使用 LRU 算法在所有數據中進行篩選。

  allkeys-lfu 策略,使用 LFU 算法在所有數據中進行篩選。

這也就是說,如果一個鍵值對被刪除策略選中了,即使它的過期時間還沒到,也需要被刪除。當然,如果它的過期時間到了但未被策略選中,同樣也會被刪除。接下來,我們就看看 volatile-lru 和 allkeys-lru 策略都用到的 LRU 算法吧。LRU 算法工作機制並不復雜,我們一起學習下。LRU 算法的全稱是 Least Recently Used,從名字上就可以看出,這是按照最近最少使用的原則來篩選數據,最不常用的數據會被篩選出來,而最近頻繁使用的數據會留在緩存中。那具體是怎么篩選的呢?LRU 會把所有的數據組織成一個鏈表,鏈表的頭和尾分別表示 MRU 端和 LRU 端,分別代表最近最常使用的數據和最近最不常用的數據。我們看一個例子。

 

 

 我們現在有數據 6、3、9、20、5。如果數據 20 和 3 被先后訪問,它們都會從現有的鏈表位置移到 MRU 端,而鏈表中在它們之前的數據則相應地往后移一位。因為,LRU 算法選擇刪除數據時,都是從 LRU 端開始,所以把剛剛被訪問的數據移到 MRU 端,就可以讓它們盡可能地留在緩存中。

如果有一個新數據 15 要被寫入緩存,但此時已經沒有緩存空間了,也就是鏈表沒有空余位置了,那么,LRU 算法做兩件事:

  數據 15 是剛被訪問的,所以它會被放到 MRU 端;

  算法把 LRU 端的數據 5 從緩存中刪除,相應的鏈表中就沒有數據 5 的記錄了。

LRU 算法在實際實現時,需要用鏈表管理所有的緩存數據,這會帶來額外的空間開銷。而且,當有數據被訪問時,需要在鏈表上把該數據移動到 MRU 端,如果有大量數據被訪問,就會帶來很多鏈表移動操作,會很耗時,進而會降低 Redis 緩存性能。

所以,在 Redis 中,LRU 算法被做了簡化,以減輕數據淘汰對緩存性能的影響。具體來說,Redis 默認會記錄每個數據的最近一次訪問的時間戳(由鍵值對數據結構 RedisObject 中的 lru 字段記錄)。然后,Redis 在決定淘汰的數據時,第一次會隨機選出 N 個數據,把它們作為一個候選集合。接下來,Redis 會比較這 N 個數據的 lru 字段,把 lru 字段值最小的數據從緩存中淘汰出去。

Redis 提供了一個配置參數 maxmemory-samples,這個參數就是 Redis 選出的數據個數 N。例如,我們執行如下命令,可以讓 Redis 選出 100 個數據作為候選數據集:

CONFIG SET maxmemory-samples 100

當需要再次淘汰數據時,Redis 需要挑選數據進入第一次淘汰時創建的候選集合。這兒的挑選標准是:能進入候選集合的數據的 lru 字段值必須小於候選集合中最小的 lru 值。當有新數據進入候選數據集后,如果候選數據集中的數據個數達到了 maxmemory-samples,Redis 就把候選數據集中 lru 字段值最小的數據淘汰出去。這樣一來,Redis 緩存不用為所有的數據維護一個大鏈表,也不用在每次數據訪問時都移動鏈表項,提升了緩存的性能。

除了使用 LFU 算法以外的 5 種緩存淘汰策略,這里有三個使用建議。

  優先使用 allkeys-lru 策略。這樣,可以充分利用 LRU 這一經典緩存算法的優勢,把最近最常訪問的數據留在緩存中,提升應用的訪問性能。如果你的業務數據中有明顯的冷熱數據區分,我建議你使用 allkeys-lru 策略。

  如果業務應用中的數據訪問頻率相差不大,沒有明顯的冷熱數據區分,建議使用 allkeys-random 策略,隨機選擇淘汰的數據就行。

  如果你的業務中有置頂的需求,比如置頂新聞、置頂視頻,那么,可以使用 volatile-lru 策略,同時不給這些置頂數據設置過期時間。這樣一來,這些需要置頂的數據一直不會被刪除,而其他數據會在過期時根據 LRU 規則進行篩選。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM