Redis Quicklist 竟讓記憶體佔用狂降50%?

公众号-JavaEdge發表於2024-10-22

0 引言

Redis 作為一種高效的記憶體型鍵值資料庫,得益於其底層資料結構的精妙設計。對於 List 型別的資料,Redis 從早期的簡單連結串列(linkedlist),到壓縮列表(ziplist),再到如今的 quicklistlistpack,不斷最佳化以平衡記憶體利用率和效能。這篇文章將深入剖析 Redis 的 quicklist 和 listpack 資料結構,幫助 Java 技術專家理解其背後的設計思想與使用場景。

Redis List 結構的演進

在 Redis 早期的版本中,List 型別的資料主要透過連結串列(LinkedList)實現,雖然連結串列在插入和刪除操作上有較高的效率,但連結串列的節點分散儲存,不利於記憶體的連續性,也會帶來較高的記憶體消耗。為了解決這些問題,Redis 引入了壓縮列表(ziplist),一個將所有元素緊湊儲存在一塊連續記憶體空間中的結構,極大地提升了記憶體利用率。

然而,隨著資料量的增加,ziplist 也暴露出了其操作上的效能瓶頸。為此,Redis 開發了 quicklist,將連結串列和壓縮列表的優勢結合。Redis 5.0 引入了 listpack,作為壓縮列表的替代方案,進一步最佳化記憶體利用率和效能。

1 Quicklist:連結串列與壓縮列表的結合

1.1 結構概覽

Quicklist 是一個結合了雙向連結串列和壓縮列表的混合結構。它將連結串列的每一個節點設計為一個壓縮列表(ziplist),這樣既保持了連結串列的插入和刪除優勢,又透過壓縮列表提高了記憶體利用率。

struct quicklist {
    quicklistNode *head;
    quicklistNode *tail;
    unsigned long count;        /* List element count */
    unsigned int len;           /* Number of quicklistNodes */
    int fill : 16;              /* fill factor for individual nodes */
    unsigned int compress : 16; /* depth of end nodes not to compress */
};

每個 quicklistNode 包含一個 ziplist,它們之間透過雙向連結串列連線。fill 引數控制每個節點中可以容納的元素數量,compress 引數決定了 quicklist 在兩端保留多少未壓縮的節點,用於提高頻繁訪問區域的效能。

1.2 操作原理

  • 插入操作:當一個元素被插入到 List 中時,Redis 會首先檢查目標 quicklistNode 中的壓縮列表是否有空間。如果空間足夠,則直接在對應的 ziplist 中進行插入操作;否則,會在當前連結串列節點之前或之後建立一個新的 quicklistNode,並將元素插入其中。
  • 刪除操作:類似於插入,刪除操作會定位到元素所在的壓縮列表進行刪除操作。如果一個 ziplist 中的元素被全部刪除,整個 quicklistNode 也會被釋放。

1.3 記憶體與效能權衡

Quicklist 的最大優勢在於其記憶體與效能的靈活平衡。透過將元素儲存在緊湊的壓縮列表中,減少了記憶體碎片問題,而雙向連結串列結構則確保了較高效的插入和刪除效能。需要注意的是,quicklist 中的壓縮列表數量受 fill 引數影響,填充因子的調優在效能和記憶體佔用之間找到平衡尤為關鍵。

2. Listpack:壓縮列表的繼任者

Redis 5.0 引入了 Listpack,一種類似於壓縮列表的資料結構,但它相比 ziplist 在設計上有更多的改進,主要用於實現 Redis 的 Sorted Set 和 Hash 中的小物件集合。

2.1 結構概覽

Listpack 是一種緊湊的、連續的記憶體儲存結構,用來存放一系列長度不固定的字串或整數。與 ziplist 類似,Listpack 也在一塊連續的記憶體中儲存資料,但其更簡化的結構設計帶來了更高的效能和更低的記憶體開銷。

struct listpack {
    unsigned char *entry_start; // Listpack entries start here
    unsigned int total_bytes;   // Total size of the listpack
    unsigned int num_entries;   // Number of entries in the listpack
};

Listpack 採用變長編碼的方式來儲存每個元素,並且每個 entry 的開銷比 ziplist 更低。其設計目標是確保在儲存小型資料集合時,比 ziplist 更加高效。

2.2 最佳化細節

  • 記憶體最佳化:Listpack 採用了更加緊湊的編碼方式,減少了元素的後設資料開銷。例如,Listpack 使用一個位元組來表示整數,而 ziplist 則可能需要額外的後設資料。
  • 效能最佳化:Listpack 的簡單結構使其在插入和刪除操作上比 ziplist 更高效,特別是在遍歷整個 Listpack 的時候,效能表現更為優異。

2.3 使用場景

Listpack 主要用於 Redis 的 Sorted Set、Hash 和 Stream 的實現中。當資料量較少時,Listpack 能夠提供優秀的記憶體利用率;當資料量增多時,Redis 會自動將其轉換為其他資料結構(如 skiplist 或 hash 表)。

3 Quicklist 與 Listpack 的對比

特性 Quicklist Listpack
結構型別 連結串列 + 壓縮列表 緊湊型連續記憶體結構
主要應用場景 Redis List Redis Sorted Set, Hash
記憶體佔用 中等,可調優 極低
插入/刪除效能 較好,連結串列提供快速操作 較好,適合小型集合
資料量增加時的行為 自動分裂為多個 ziplist 轉換為複雜結構(如 skiplist)

4 Java 開發者的思考:資料結構選擇的啟示

對於 Java 開發者來說,Redis 的 quicklist 和 listpack 設計提供了許多資料結構設計上的啟發:

  • 記憶體與效能的平衡:Redis 的 quicklist 透過結合連結串列與緊湊列表實現了記憶體利用率與操作效能之間的平衡。在 Java 開發中,類似的權衡也可以用於選擇合適的資料結構。對於小型集合,緊湊儲存能夠有效降低記憶體佔用;而對於大型集合或頻繁插入/刪除的場景,連結串列或其他高效的資料結構則更加適合。
  • 最佳化快取命中率:quicklist 透過緊湊儲存元素,提升了 CPU 快取的利用率。這種思想在 Java 應用中也可以借鑑,尤其是在對效能要求較高的系統中,合理設計資料結構以最大化利用 CPU 快取是提升效能的關鍵。
  • 變長編碼的高效性:Listpack 採用變長編碼方式儲存資料,減少了儲存小型整數或短字串的開銷。在 Java 開發中,類似的思想也可以透過使用合適的序列化策略或者最佳化物件的儲存格式來實現。

5 總結

Redis 的 quicklist 和 listpack 透過不同的設計策略,分別在記憶體利用和效能最佳化上提供了獨特的解決方案。對於 Java 技術專家來說,理解這些底層資料結構的設計不僅有助於更好地使用 Redis,也為開發高效能應用提供了寶貴的借鑑。透過學習這些最佳化思路,我們可以在自己的系統設計中更好地權衡記憶體與效能,選擇合適的資料結構來滿足不同場景的需求。

關注我,緊跟本系列專欄文章,咱們下篇再續!

作者簡介:魔都架構師,多家大廠後端一線研發經驗,在分散式系統設計、資料平臺架構和AI應用開發等領域都有豐富實踐經驗。

各大技術社群頭部專家博主。具有豐富的引領團隊經驗,深厚業務架構和解決方案的積累。

負責:

  • 中央/分銷預訂系統效能最佳化
  • 活動&券等營銷中臺建設
  • 交易平臺及資料中臺等架構和開發設計
  • 車聯網核心平臺-物聯網連線平臺、大資料平臺架構設計及最佳化
  • LLM Agent應用開發
  • 區塊鏈應用開發
  • 大資料開發挖掘經驗
  • 推薦系統專案

目前主攻市級軟體專案設計、構建服務全社會的應用系統。

參考:

  • 程式設計嚴選網

本文由部落格一文多發平臺 OpenWrite 釋出!

相關文章