底層:壓縮列表ziplist、intset、緊湊列表listpack
ziplist和它的級聯更新
ziplist是hash類型和list類型在底層的實現之一。當list中只包含少量元素,且元素要么是小整數要么是短字符串時,此時list底層就采用ziplist的方式存儲。當hash只保存少量鍵值對,而且每個鍵值對要么是小整數要么是短字符串時,此時hash底層就采用ziplist存儲。
列表定義
壓縮列表是redis為了節約內存而開發的,是由連續內存塊組成的順序型數據結構,一個壓縮列表包含多個entry,每個entry可以保存一個字節數組或者一個整數值。
下圖代表壓縮列表的各組成部分和詳細說明:

下圖表示了一個壓縮列表示例:

zlbytes的值是80,代表壓縮列表的總長為80字節;zltail的值是60,代表從起始指針p開始偏移60,就能找到最后一個entry的位置;zllen的值是3,代表壓縮列表有3個節點。
entry定義
entry分為三個部分:

previous_entry_length以字節為單位記錄了前一個節點的長度,如果前一個節點的長度小於254字節,那么該字段就占1字節的空間,前一節點的長度就保存在這個字節中;如果前一個節點的長度大於等於254字節,那么該字段的就占5字節的空間,其中屬性的第一個字節被設置為0xFE,也就是十進制254,之后的四個字節則用於保存前一個節點的長度。壓縮列表根據這個字段就可以很輕松的從表尾節點遍歷到表頭節點。
encoding屬性記錄了節點保存的數據類型和長度,entry可以保存字節數組和整數,encoding的具體含義和content中保存的值的關系如下:

其中開頭為11代表存的是整數,開頭為00、01、10分別代表不同長度的字節數組,字節數組的具體長度由去掉前兩位的編碼來表示。如00001011中的00代表content中是一個長度小於等於63字節的字節數組,該數組的具體長度是001011,也就是11.
級聯更新
級聯更新是現有壓縮列表構造的一個弊端。
這個弊端源於previous_entry_length屬性的設定,這個屬性可能占1字節,也可能占5字節,實際占用空間大小和前一個節點大小有關。如果在某個位置新插入一個較大的節點,或者刪除一個大節點后的節點,可能就會導致后面節點的previous_entry_length屬性由1字節變成5字節,而因為該屬性的改變也會導致該entry大小的改變,從而可能引發該entry大於等於254字節,進而導致后面的entry大小繼續改變。這個改變可能會波及到整個壓縮列表,所以稱之為級聯更新。
級聯更新在最壞情況下需要對壓縮列表執行N次空間重分配操作,每次重分配的復雜度是ON,級聯更新最壞復雜度為ON方。
雖然級聯更新存在,但是出現概率很低,幾乎可以忽略不計。
intset
intset是set類型的底層實現之一,當set只包含整數值元素,且數量不多時就會采用intset形式存儲。
intset是一種用於保存整數值的集合,它可以保存類型為int16_t、int32_t、int64_t的整數值,並且保證集合中不會出現重復元素。
定義
intset結構定義如下:
typedef struct intset{
//編碼方式
uint32_t encoding;
//集合包含元素的數量
uint32_t length;
//保存元素的數組
int8_t contents[];
}
contents是用來保存整數的,其中保存的數字從小到大排列,不含任何重復項。雖然contents被聲明為int8_t,但是contents卻不保存任何int8_t類型的值,它的真正類型取決於encoding的值,它可以保存類型為int16_t、int32_t、int64_t的整數值。
下圖代表一個保存值類型為int16_t的,元素個數為5的intset,此時數組的大小是5*16=80位。

下圖代表一個保存值類型為int64_t的,元素個數為4的intset,此時數組的大小是4*64=256位。

升級
當把一個新元素加入intset中時,如果intset當前的類型無法滿足該類型所需要的空間時,intset就會執行升級操作。如當我們往intset中存入1、3、5時,此時intset底層存儲的是int16_t類型,當我們存入一個很大的數,必須用int64_t類型表達的時候,此時contents數組保存的數值類型就會全部變為int64_t的。
升級的大致步驟如下:
1、根據元素的新類型,創造新的數組空間。
2、將底層數組的所有元素都轉換為新類型,然后將轉換后的元素移動到新數組中,在這個過程中保持有序性質不變。
3、最后將新元素添加到底層數組。
因為要添加的新值一定是突破原來類型的,所以它要么比現有的所有元素小,要么比現有的所有元素都大,所以新元素的位置一定是在數組的開頭或者末尾。
升級的好處主要有兩點:
1、通過底層類型的動態調整,我們可以將不同類型的數據放入redis中,而不必保持一種類型,靈活性很強。
2、節省空間。
值得注意的是,intset沒有降級操作,即使引發升級的那個新元素被刪除了,底層的編碼方式還是保持不變。
listpack
listpack是對ziplist的改進,它比ziplist少了一個定位最后一個元素的屬性(最后一個元素距離起始偏移量),listpack無需這個字段輔助逆序遍歷。它的entry將長度字段放在了尾部,且記錄的是本entry的長度,有了它就可以快速定位前一個entry的末尾,依然可以逆序遍歷(在listpack定位到最后一個元素只需要總長度減去最后一個entry的長度即可)。這樣本entry的屬性不會出現在其他entry中了,消除了級聯更新。但listpack目前只用在stream中。