Redis 內存壓縮原理

Redis 無疑是一個大量消耗內存的數據庫,所以 Redis 引入了一些設計巧妙的數據結構進行內存壓縮來減輕負擔。ziplist、quicklist 以及 intset 是其中最經常使用最重要的壓縮存儲結構。git

瞭解編碼類型

Redis對外提供了 string, list, hash, set, zset等數據類型,每種數據類型可能存在多種不一樣的底層實現,這些底層數據結構被稱爲編碼(encoding)。github

以 list 類型爲例,其經典的實現方式爲雙向鏈表(linkedlist)。雙向鏈表的每一個節點擁有一個前向指針一個後向指針,在64位系統下每一個節點佔用了 2 * 64bit = 16 Byte 的額外空間。所以當 list 中元素較少時會使用 ziplist 做爲底層數據結構。redis

object encoding <key> 命令能夠查看某個 key 的編碼類型:算法

127.0.0.1:6379> set a 1
OK
127.0.0.1:6379> object encoding a
"int"
127.0.0.1:6379> rpush l 1
(integer) 1
127.0.0.1:6379> object encoding l
"ziplist"

先總結一下各類數據結構可使用的編碼類型,下文再對這些壓縮類型進行詳細說明:數據庫

  • string
    • raw: 動態字符串(SDS)
    • embstr: 優化內存分配的字符串編碼
    • int: 整數
  • list
    • linkedlist
    • ziplist
    • quicklist
  • set
    • hashtable
    • intset
  • hash
    • ziplist
    • hashtable
  • zset(sortedset)
    • ziplist
    • skiplist

本文接下來將詳細說明各類壓縮編碼的原理以及編碼決定規則。數組

ziplist

ziplist 是一段連續內存,相似於數組結構。當元素比較少時使用數組結構不只節省內存,並且遍歷操做的開銷也不大。所以 list, hash, zset 在元素較少時都採用 ziplist 存儲。數據結構

ziplist 的源碼能夠在: redis/ziplist.c 中找到。flex

ziplist 存儲爲一段裸二進制數據(unsigned char *), 能夠看到源代碼中大量使用宏進行定義,雖然節省了大量內存可是代碼可讀性較低。優化

ziplist 的結構:ui

<zlbytes> <zltail> <zllen> <entry> <entry> ... <entry> <zlend>
  • zlbytes: uint32 型, 存儲整個ziplist當前被分配的空間,包含自身佔用的4個字節。
  • zltail: uint32 型, 存儲ziplist中最後一個entry相對頭部的偏移量, 用於直接訪問尾端元素避免遍歷。
  • zllen: uint16 型, 記錄 ziplist 中元素的個數
  • entry: 實際存儲元素的單元
  • zlend: 魔法數字 255 標記 ziplist 的結尾, 沒有 entry 以 0xff 開頭不會出現誤判的問題

entry 是實際存儲數據的單元, 能夠存儲 int 或 string 類型數據。在存儲 string 類型數據時 entry 的結構爲:

  • prevlen: 表示前一個 entry 的長度,用於從後向前遍歷。
  • encoding: 存儲當前 entry 的數據類型和長度
  • entry-data: 實際的數據部分

當存儲 int 類型的數據時, 數據(entry-data)會被合併到 encoding 內部,此時沒有 entry-data 字段。

當前一個元素長度小於254(255用於zlend)時,prevlen長度爲1個字節,值爲前一個entry的長度;若是長度大於等於254,prevlen 用5個字節表示,第一字節設置爲254,後面4個字節存儲一個小端的無符號整型,表示前一個entry的長度。

encoding 用來表示 entry 的數據類型和長度。encoding 的所有定義能夠在 ziplist.c 中找到。

下面列出幾種 encoding 的示例,encoding 中的字母表示一個bit:

  • 00pppppp: encoding 的長度爲一個字節,後6位表示字符串的長度。由於長度最多6位,所以字符串的長度不超過63
  • 01pppppp qqqqqqqq: encoding 的長度爲兩個字節, 後14位存儲字符串的長度,所以字符串的長度不超過16383
  • 11000000: encoding爲3個字節,後2個字節表示一個int16
  • 1110000: encoding爲4個字節,後3個字節表示一個有符號整型
  • 11111111: zlend

前面提到每一個 entry 都會有一個 prevlen 字段存儲前一個 entry 的長度。若是內容小於 254 字節,prevlen 用 1 字節存儲,不然就是 5 字節。這意味着若是某個 entry 通過了修改操做從 253 字節變成了 254 字節,那麼它的下一個 entry 的 prevlen 字段就要更新,從 1 個字節擴展到 5 個字節;若是這個 entry 的長度原本也是 253 字節,那麼後面 entry 的 prevlen 字段還得繼續更新。這種現象被稱爲 ziplist 的級聯更新,添加、修改、刪除元素的操做都有可能致使級聯更新。

ziplist 不會預留擴展空間,每次插入一個新的元素就須要調用 realloc 擴展內存, 並可能須要將原有內容拷貝到新地址。

綜上,ziplist 是一個使用連續內存存儲數據,相似於數組的數據結構。能夠 O(1) 的時間複雜度訪問首尾元素。由於 entry 長度不肯定,能夠向前或向後順序訪問,不能隨機訪問。由於級聯更新的現象的存在,添加、修改、刪除元素操做的複雜度在 O(n) 到 O(n^2) 之間。

在知足下列條件時, list, hash 和 sortedset 三種結構會採用 ziplist 編碼:

  • list: value 字節數 <= list-max-ziplist-value 且 元素數 <= list-max-ziplist-entries
  • hash: value 字節數 <= hash-max-ziplist-value 且 元素數 <= hash-max-ziplist-entries
  • zset: value 字節數 <= zset-max-ziplist-value 且 元素數 <= zset-max-ziplist-entries

ziplist 存儲 list 時每一個元素會做爲一個 entry; 存儲 hash 時 key 和 value 會做爲相鄰的兩個 entry; 存儲 zset 時 member 和 score 會做爲相鄰的兩個entry。

當不知足上述條件時,ziplist 會升級爲 linkedlist, hashtable 或 skiplist 編碼。在任何狀況下大內存的編碼都不會降級爲 ziplist。

quicklist

Redis 3.2 版本引入了 quicklist 做爲 list 的底層實現,再也不使用 linkedlist 和 ziplist 實現。quicklist 是 ziplist 組成的雙向鏈表,它的每一個節點都是一個 ziplist。

quicklist 是結合了 linkedlist 和 ziplist 優勢的產物:

  • linkedlist 便於進行增刪改操做可是內存佔用較大
  • ziplist 內存佔用較少,可是由於每次修改均可能觸發 realloc 和 memcopy, 而且可能致使級聯更新。所以修改操做的效率較低,在 ziplist 較長時這個問題更加突出。

因而每一個節點上 ziplist 的大小變成了一個須要折中的難題:

  • ziplist 越小,quicklist 越接近於 linkedlist。此時存儲效率降低,可是修改操做的效率較高。
  • ziplist 越大,quicklist 越接近於 ziplist。此時存儲效率上升,可是修改操做的效率下降。

redis 根據 list-max-ziplist-size 配置項來決定節點上 ziplist 的長度。

list-max-ziplist-size 爲正值的時候,表示按照數據項個數來限定每一個 quicklist 節點上的 ziplist 長度。好比,當這個參數配置成5的時候,表示每一個 quicklist 節點的ziplist 最多包含5個數據項。

當爲負值的時候,表示按照佔用字節數來限定每一個節點上的 ziplist 長度。這時,它只能取 -1 到 -5 這五個值:

  • -5: 每一個節點上的 ziplist 大小不能超過64 KB
  • -4: 每一個節點上的 ziplist 大小不能超過 32 KB。
  • -3: 每一個節點上的 ziplist 大小不能超過16 Kb。
  • -2: 每一個節點上的 ziplist 大小不能超過8 Kb。這是 redis 的默認設置。
  • -1: 每一個節點上的 ziplist 大小不能超過4 Kb。

壓縮中間節點

對於一個很長的列表而言,最常使用的是其兩端的數據,中間數據被訪問的機率較低。所以,quicklist 容許將中間的節點使用 LZF 算法進行壓縮以節省內存。

list-compress-depth 表示quicklist兩端不被壓縮的節點個數:

  • 0: 表示都不壓縮。這是Redis的默認值。
  • 1: 表示quicklist兩端各有1個節點不壓縮,中間的節點壓縮。
  • 2: 表示quicklist兩端各有2個節點不壓縮,中間的節點壓縮。
  • 以此類推...

intset

當集合中的元素均爲整數且元素數少於 set-max-intset-entries 時,redis 採用 inset 編碼存儲集合。當插入非整數元素或元素數超過閾值後,intset 會升級爲 hashtable 編碼進行存儲。

intset 的源碼能夠在: redis/intset.c 中找到。

intset 是整數元素組成的有序數組, 能夠支持 O(logn) 級別的查詢。

intset 的內存結構與 ziplist 相似是一段的內存。它由三個部分組成:

  • encoding: 表示intset中的每一個數據元素用幾個字節來存儲。它有三種可能的取值:
    • INTSET_ENC_INT16表示每一個元素用2個字節存儲
    • INTSET_ENC_INT32表示每一個元素用4個字節存儲
    • INTSET_ENC_INT64表示每一個元素用8個字節存儲。
  • length: 表示intset中的元素個數。encoding和length兩個字段構成了intset的頭部(header)。
  • contents: 表示實際存儲的內容。它是一個C語言的柔性數組(flexible array member)

須要注意的是,每次添加元素 intset 都會檢查是否須要將 INTSET_ENCODING 升級爲更長的整數。與每一個 entry 擁有獨立 encoding 的 ziplist 不一樣,inset 中全部成員使用統一的 encoding。

相關文章
相關標籤/搜索