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"
先總結一下各類數據結構可使用的編碼類型,下文再對這些壓縮類型進行詳細說明:數據庫
本文接下來將詳細說明各類壓縮編碼的原理以及編碼決定規則。數組
ziplist 是一段連續內存,相似於數組結構。當元素比較少時使用數組結構不只節省內存,並且遍歷操做的開銷也不大。所以 list, hash, zset 在元素較少時都採用 ziplist 存儲。數據結構
ziplist 的源碼能夠在: redis/ziplist.c 中找到。flex
ziplist 存儲爲一段裸二進制數據(unsigned char *), 能夠看到源代碼中大量使用宏進行定義,雖然節省了大量內存可是代碼可讀性較低。優化
ziplist 的結構:ui
<zlbytes> <zltail> <zllen> <entry> <entry> ... <entry> <zlend>
entry 是實際存儲數據的單元, 能夠存儲 int 或 string 類型數據。在存儲 string 類型數據時 entry 的結構爲:
當存儲 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:
前面提到每一個 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 編碼:
ziplist 存儲 list 時每一個元素會做爲一個 entry; 存儲 hash 時 key 和 value 會做爲相鄰的兩個 entry; 存儲 zset 時 member 和 score 會做爲相鄰的兩個entry。
當不知足上述條件時,ziplist 會升級爲 linkedlist, hashtable 或 skiplist 編碼。在任何狀況下大內存的編碼都不會降級爲 ziplist。
Redis 3.2 版本引入了 quicklist 做爲 list 的底層實現,再也不使用 linkedlist 和 ziplist 實現。quicklist 是 ziplist 組成的雙向鏈表,它的每一個節點都是一個 ziplist。
quicklist 是結合了 linkedlist 和 ziplist 優勢的產物:
因而每一個節點上 ziplist 的大小變成了一個須要折中的難題:
redis 根據 list-max-ziplist-size
配置項來決定節點上 ziplist 的長度。
當 list-max-ziplist-size
爲正值的時候,表示按照數據項個數來限定每一個 quicklist 節點上的 ziplist 長度。好比,當這個參數配置成5的時候,表示每一個 quicklist 節點的ziplist 最多包含5個數據項。
當爲負值的時候,表示按照佔用字節數來限定每一個節點上的 ziplist 長度。這時,它只能取 -1 到 -5 這五個值:
壓縮中間節點
對於一個很長的列表而言,最常使用的是其兩端的數據,中間數據被訪問的機率較低。所以,quicklist 容許將中間的節點使用 LZF 算法進行壓縮以節省內存。
list-compress-depth
表示quicklist兩端不被壓縮的節點個數:
當集合中的元素均爲整數且元素數少於 set-max-intset-entries
時,redis 採用 inset 編碼存儲集合。當插入非整數元素或元素數超過閾值後,intset 會升級爲 hashtable 編碼進行存儲。
intset 的源碼能夠在: redis/intset.c 中找到。
intset 是整數元素組成的有序數組, 能夠支持 O(logn) 級別的查詢。
intset 的內存結構與 ziplist 相似是一段的內存。它由三個部分組成:
須要注意的是,每次添加元素 intset 都會檢查是否須要將 INTSET_ENCODING 升級爲更長的整數。與每一個 entry 擁有獨立 encoding 的 ziplist 不一樣,inset 中全部成員使用統一的 encoding。