Redis實現之壓縮列表

來源:互聯網
上載者:User

標籤:記憶體   aaaaa   多次   table   oid   索引值   file   留空   order   

壓縮列表

壓縮列表(ziplist)是列表鍵和雜湊鍵的底層實現之一,當一個列表鍵只包含少量清單項目,並且每個清單項目要嘛是整數值,要嘛是比較短的字串,那麼Redis就會使用壓縮列表來做列表鍵的底層實現。例如,執行以下命令將建立一個壓縮列表鍵的底層實現

127.0.0.1:6379> RPUSH lst 1 3 5 10086 "hello" "world"(integer) 6127.0.0.1:6379> OBJECT ENCODING lst"quicklist"

  

quicklist結構在quicklist.c中的解釋為A doubly linked list of ziplists意思為一個由ziplist組成的雙向鏈表,列表鍵裡麵包含的都是1、3、5、10086這樣的小整數值,以及"hello"、"world"這樣短的字串。

另外,當一個雜湊鍵只包含少量索引值對,且每個索引值對的鍵和值要嘛是比較短的字串,Redis會使用壓縮列表來做雜湊鍵的底層實現。舉個栗子,執行以下命令將建立一個壓縮列表實現的雜湊鍵

127.0.0.1:6379> HMSET profile "name" "Jack" "age" 28 "job" "Programmer"OK127.0.0.1:6379> OBJECT ENCODING profile"ziplist"

  

雜湊鍵裡麵包含的所有鍵和值都是小整數或者短字串

壓縮列表的構成

壓縮列表是Redis為了節約記憶體而開發的,是由一系列特殊編碼的連續記憶體塊組成的順序型資料結構。一個壓縮列表可以包含任意多個節點,每個節點可以儲存一個位元組數組或者一個整數值。圖1-1展示了壓縮列表的各個組成部分,表1-1則記錄了各個組成部分的類型、長度以及用途

圖1-1   壓縮列表的各個組成部分

表1-1   壓縮列表各個組成部分的詳細說明明
屬性 類型 長度 用途
zlbytes uint32_t 4位元組 記錄整個壓縮列表佔用的記憶體位元組數:在對壓縮列表進行記憶體重分配,或者計算zlend的位置時使用 
zltail  uint32_t 4位元組  記錄壓縮列表表尾節點距離壓縮列表的起始地址有多少位元組:通過這個位移量,程式無須遍曆整個壓縮列表就可以確定表尾節點的地址 
zllen   uint16_t 2位元組 

記錄了壓縮列表包含的節點數量:當這個屬性的值小於UINT16_MAX(65535)時, 這個屬性的值就是壓縮列表包含節點的數量;

這個值等於 UINT16_MAX 時,節點的真實數量需要遍曆整個壓縮列表才能計算得出

entryX  列表節點  不定  壓縮列表包含的各個節點,節點的長度由節點儲存的內容決定 
zlend  uint8_t   1位元組  特殊值0xFF(十進位 255 ),用於標記壓縮列表的末端

圖1-2展示了一個壓縮列表的樣本:

  • 列表zlbytes屬性的值為0x50(十進位80),表示壓縮列表的總長為80位元組
  • 列表zltail屬性的值為0x3c(十進位60),表示如果有一個指向壓縮列表起始地址的指標p,那麼只要用指標p加上偏量60,就可以計算出表尾節點entry3的地址
  • 列表zlend屬性的值為0x3(十進位3),表示壓縮列表包含三個節點

圖1-2   包含三個節點的壓縮列表

圖1-3展示了另一個壓縮表示例

圖1-3   包含五個節點的壓縮列表

  • 列表zlbytes屬性的值為0xd2(十進位210),表示壓縮列表的總長為210位元組
  • 列表zltail屬性的值為0xb3(十進位179),表示如果有一個指向壓縮列表起始地址的指標p,那麼只要用指標p加上偏量179,就可以計算出表尾節點entry5的地址
  • 列表zlend屬性的值為0x5(十進位5),表示壓縮列表包含五個節點

壓縮列表節點的構成

每個壓縮列表節點可以儲存一個位元組數組或者一個整數值,其中,位元組數組可以是以下三種長度的其中一種:

  • 長度小於等於63(2^6 -1)位元組的位元組數組
  • 長度小於等於16 383(2^14 -1)位元組的位元組數組
  • 長度小於等於4 294 967 295(2^32 -1)位元組的位元組數組

而整數值則可以是以下六種長度的其中一種:

  • 4位長,介意0至12之間的不帶正負號的整數
  • 1位元組長的有符號整數
  • 3位元組長的有符號整數
  • int16_t類型整數
  • int32_t類型整數
  • int64_t類型整數

每個壓縮列表節點都由previous_entry_length、encoding、content三個部分組成,1-4所示

圖1-4   壓縮列表節點的各個組成部分

previous_entry_length

節點previous_entry_length屬性以位元組為單位,記錄了壓縮列表中前一個節點的長度。previous_entry_length屬性可以是1個位元組或者5個位元組:

  • 如果前一節點的長度小於254位元組,那麼previous_entry_length屬性的長度為1位元組:前一節點的長度就儲存在這一個位元組裡面
  • 如果前一節點的長度大於等於254位元組,那麼previous_entry_length屬性的長度為5位元組:其中屬性的第一位元組會被設定為0xFE(十進位值254),而之後的四個位元組則用於儲存前一節點的長度

圖1-5展示了一個包含一位元組長previous_entry_length屬性的壓縮列表節點,屬性的值為0x05,表示前一節點的長度為5位元組

圖1-5   當前節點的前一節點的長度為5位元組

 

圖1-6展示了一個包含五位元組長previous_entry_length屬性的壓縮節點,屬性值為0xFE00002766,其中值的最高位位元組0xFE表示這是一個五位元組長的previous_entry_length屬性,而之後的四位元組00002766(十進位10086)才是前一節點的實際長度。因為節點的previous_entry_length屬性記錄了前一個節點的長度,所以程式可以通過指標運算,根據當前節點的起始位置來計算出前一個節點的起始地址

舉個栗子,如果我們有一個指向當前節點起始地址的指標c,那麼我們只要用指標c減去當前節點previous_entry_length屬性的值,就可以得出一個指向前一個節點起始地址的指標p,1-7所示

圖1-7   通過指標運算計算處前一個節點的地址

壓縮列表的表尾向表頭遍曆操作就是使用這一原理實現的,只要我們擁有一個指向某個節點起始地址的指標,那麼通過這個指標以及這個節點的previous_entry_length屬性,程式就可以一直向前一個節點回溯,最終到達壓縮列表的表前端節點。

圖1-8展示了一個從表尾節點向表前端節點進行遍曆的完整過程:

  • 首先,我們擁有指向壓縮列表表尾節點entry4起始地址的指標p1(指向表尾節點的指標可以通過指向壓縮列表起始地址的指標加上zltail屬性的值得出)
  • 通過用p1減去entry4節點previous_entry_length屬性的值,我們得到一個指向entry4前一節點entry3起始地址的指標p2
  • 通過用p2減去entry3節點previous_entry_length屬性的值,我們得到一個指向entry3前一節點entry2起始地址的指標p3
  • 通過用p3減去entry2節點previous_entry_length屬性的值,我們得到一個指向entry2前一節點entry1起始地址的指標p4,entry1為壓縮列表的表前端節點
  • 最終,我們從表尾節點向表前端節點遍曆了整個列表

圖1-8   一個從表尾向表頭遍曆的例子

encoding

節點的encoding屬性記錄了節點的content屬性所儲存資料的類型以及長度:

  • 一位元組、兩位元組或五位元組長,值的最高位為00、01或者10的是位元組數組編碼:這種編碼錶示節點的content屬性儲存著位元組數組,數組的長度由編碼除去最高兩位之後的其他位記錄
  • 一位元組長,值最高位以11開頭的是整數編碼:這種編碼錶示節點的content屬性儲存的是整數值,整數值的類型和長度由編碼除去最高兩位之後的其他位記錄

表1-2記錄了所有可用的位元組數組編碼,而表1-3則記錄了所有可用的整數編碼。表格中的“_”表示留空,而b、x等變數則代表實際的位元據,為了方便閱讀,多個位元組之間用空格隔開

表1-2   位元組數組編碼
編碼 編碼長度 content屬性儲存的值
00bbbbbb 1位元組 長度小於等於63位元組的位元組數組
01bbbbbb xxxxxxxx 2位元組 長度小於等於16383位元組的位元組數組
10______ aaaaaaaa bbbbbbbb cccccccc dddddddd 5位元組 長度小於等於4294967295的位元組數組

 

表1-3   整數編碼
編碼 編碼長度 content屬性儲存的值
11000000 1位元組 int16_t類型的整數
11010000 1位元組 int32_t類型的整數
11100000 1位元組 int64_t類型的整數
11110000 1位元組 24位有符號整數
11111110 1位元組 8位有符號整數
1111xxxx 1位元組 使用這一編碼的節點沒有相應的content屬性,因為編碼本身的xxxx四個位已經儲存了一個介於0和12之間的值,所以它無須content屬性

content

節點的content屬性負責儲存節點的值,節點值可以是一個位元組數組或整數,值的類型和長度由節點的encoding屬性決定

圖1-9展示了一個儲存位元組數組的節點樣本:

  • 編碼的最高位00表示節點儲存的是一個位元組數組
  • 編碼的後六位001011記錄了位元組數組的長度11
  • content屬性儲存著節點的值"hello world"

圖1-9   儲存著位元組數組"hello world"的節點

圖1-10展示了一個儲存整數值的節點樣本:

  • 編碼11000000表示節點儲存的是一個int16_t類型的整數值
  • content屬性儲存著節點的值10086

圖1-10   儲存著整數值10086的節點

連鎖更新

前面說過,每個節點的previous_entry_length屬性都記錄了前一個節點的長度:

  • 如果前一節點的長度小於254位元組,那麼previous_entry_length屬性需要用1位元組長的空間來儲存這個長度值
  • 如果前一節點的長度大於等於254位元組,那麼previous_entry_length屬性需要用5位元組長的空間來儲存這個長度值

現在,考慮這樣一種情況:在一個壓縮列表中,有多個連續的、長度介於250位元組到253位元組之間的節點e1到eN,1-11所示

圖1-11   包含e1至eN的壓縮列表

因為e1至eN的所有節點的長度都小於254位元組,所以記錄這些節點的長度只需一個位元組長的previous_entry_length屬性,換言之,e1到eN的previous_entry_length屬性都是1位元組長。這時,如果我們將一個長度大於254位元組的新節點new設定為壓縮列表的表前端節點,那麼new將成為e1的前置節點,1-12所示

圖1-12   添加新節點到壓縮列表

因為e1的previous_entry_length屬性僅1位元組長,它沒辦法儲存新節點new的長度,所以程式將對壓縮列表執行空間重分配操作,並將e1的節點previous_entry_length屬性從原來的1位元組長擴充為5位元組長

現在,麻煩來了,e1原本的長度介於250位元組至253位元組之間,在為previous_entry_length屬性新增四位元組的空間之後,e1的長度就變成了介於254位元組至257位元組之間,而這種長度使用1位元組長的previous_entry_length屬性時沒法儲存的。因此,為了讓e2的previous_entry_length屬性可以記錄下e1的長度,程式需再次對壓縮列表執行空間重分配操作,並將e2節點的previous_entry_length屬性從原來的1位元組擴充為5位元組長

正如擴充e1引發對e2的擴充一樣,擴充e2也會引發對e3的擴充……為了讓每個節點的previous_entry_length屬性都符合壓縮列表對節點的要求,程式需要不斷地對壓縮列表執行空間重分配操作,直到eN為止

Redis將這種在特殊情況產生的連續多次空間擴充操作稱之為“連鎖更新”,圖1-13展示了這一過程

圖1-13   連鎖更新過程

除了添加節點可能會引發連鎖更新,刪除節點也可能會引發連鎖更新。考慮圖1-14所示的壓縮列表,如果e1至eN都是大小介於250位元組至253位元組的節點,big節點的長度大於等於254位元組(需要5位元組的previous_entry_length來儲存),而small節點的長度小於254位元組(只需要1位元組的previous_entry_length來儲存),那麼當我們將small節點從壓縮列表中刪除之後,為了讓e1的previous_entry_length屬性可以記錄big節點的長度,程式將擴充e1的空間,並由此引發之後的連鎖更新

 

圖1-14   另一種連鎖更新的情況

因為連鎖更新在最壞情況下需要對壓縮列表執行N次空間重分配操作,而每次空間重分配的最壞時間複雜度為O(N),所以連鎖更新的最壞複雜度為O(N^2)。要注意的是,儘管連鎖更新的複雜度較高,但它真正造成效能問題的機率是很低的:

  • 首先,壓縮列表裡要恰好有多個連續的、長度介於250位元組至253位元組之間的節點,連鎖更新才有可能引發,在實際中,這種能夠情況並不多見
  • 其次,即使出現連鎖更新,但只要被更新的節點數量不多,就不會對效能造成任何影響:比如說,對三五個節點進行連鎖更新是絕不會影響效能的

因為以上原因,ziplistPush等命令的平均時間複雜度僅為O(N),在實際中,我們可以放心使用這些函數,而不必擔心連鎖更新會影響壓縮列表的效能

壓縮列表API

表1-4列出了所有用於操作壓縮列表的API

 

包1-4   壓縮列表API
函數 作用 時間複雜度
ziplistNew(void) 建立一個新的壓縮列表 O(1)
ziplistPush(unsigned char *zl, unsigned char *s, unsigned int slen, int where) 建立一個包含給定值的新節點,並將這個新節點添加到壓縮列表的表頭或者表尾 平均O(N),最壞O(N^2)
ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) 將包含給定值的新節點插入到給定節點之後 平均O(N),最壞O(N^2)
ziplistIndex(unsigned char *zl, int index) 返回壓縮列表給定索引上的節點 O(N)
ziplistFind(unsigned char *p, unsigned char *vstr, unsigned int vlen, unsigned int skip) 在壓縮列表中尋找並返回包含了給定值的節點 因為節點的值可能是一個位元組數組,所以檢查節點值和給定值是否相同的複雜度為O(N), 而尋找整個列表的複雜度則為O(N^2)
ziplistNext(unsigned char *zl, unsigned char *p) 返回給定節點的下一個節點 O(1)
ziplistPrev(unsigned char *zl, unsigned char *p) 返回給定節點的前一個節點 O(1)
ziplistGet(unsigned char *p, unsigned char **sval, unsigned int *slen, long long *lval) 擷取給定節點所儲存的值 O(1)
ziplistDelete(unsigned char *zl, unsigned char **p) 從壓縮列表中刪除給定的節點 平均O(N),最壞O(N^2)
ziplistDeleteRange(unsigned char *zl, unsigned int index, unsigned int num) 刪除壓縮列表在給定索引上的連續多個節點 平均O(N),最壞O(N^2)
ziplistBlobLen(unsigned char *zl) 返回壓縮列表目前佔用的記憶體位元組數 O(1)
ziplistLen(unsigned char *zl) 返回壓縮列表目前包含的節點數量 節點數量小於65535時O(1),大於65535時O(N)

Redis實現之壓縮列表

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.