Redis | 第一部分:資料結構與物件 中篇《Redis設計與實現》
阿新 • • 發佈:2021-11-20
目錄
新人制作,如有錯誤,歡迎指出,感激不盡!
歡迎關注公眾號,會分享一些更日常的東西!
如需轉載,請標註出處!
前言
參考資料:《Redis設計與實現 第二版》;
本篇筆記按照書裡的脈絡,將知識點分為四個部分。其中第一部分資料結構與物件分為上中下篇,上篇包括:SDS、連結串列和字典;中篇包括跳躍表、整數集合和壓縮列表;下篇為物件;
上篇的連結:https://www.cnblogs.com/dlhjw/p/15569578.html
1. 跳躍表
- 跳躍表支援平均 O(logN)、最壞 O(N) 複雜度的節點查詢,還可以通過順序性操作來批量處理節點;
- 跳躍表的效率可以媲美平衡樹,實現比平衡樹簡單;
- 跳躍表在Redis裡只有兩個應用:有序集合鍵的底層實現、叢集節點中用作內部資料結構;
1.1 跳躍表與其節點的定義
-
跳躍表的定義,在
redis.h/zskiplist
結構裡:typedef struct zskiplist { //表頭節點和表尾節點 structz skiplistNode *header, *tail; //表中節點的數量(不包括表頭指標) unsigned long length; //表中層數最大的節點的層數(不包括表頭指標) int level; } zskiplist;
-
跳躍表節點的定義,在
redis.h/zskiplistNode
typedef struct zskiplistNode{ //後退指標 struct zskiplistNode *backwars; //分值 double score; //成員物件 robj *obj; //層 struct zskiplistLevel{ //前進指標 struct zskiplistNode *forward; //跨度 unsigned int apan; } level[]; } askiplistNode;
- 節點中使用
L1
、L2
、L3
等來標記節點的各個層- 帶數字的箭頭為前進指標,數字為跨度;
- 一般來說,層數越多訪問其他節點速度越快;
- 建立新跳躍表節點時,隨機生成介於1和32之間的數作為level陣列的大小;
- 跨度與遍歷無關,與排位
rank
有關。查詢某個節點時,將沿途層相加,得到排位;
- 帶
BW
字樣的為後退指標; 1.0
、2.0
、3.0
為分值,分值從小到大排列;- 當分值相同時,成員物件在字典中排序小的靠近表頭節點;
o1
、o2
、o3
等是成員物件,成員物件必須唯一;- 表頭節點也有後退指標、分值和成員物件,不會用到所以圖中沒有顯示;
- 下圖中
level
為5是因為o3物件有5層,為該跳躍表中最大層;
- 節點中使用
1.2 跳躍表的API
函式 | 作用 | 時間複雜度 |
---|---|---|
zslCreate | 建立一個新的跳躍表 | O(1) |
zslFree | 釋放給定跳躍表,以及表中包含的所有節點 | O(N),N為跳躍表的長度 |
zslInsert | 將包含給定成員和分值的新節點新增到跳躍表中 | 平均O(logN),最壞O(N),N為跳躍表長度 |
zslDelete | 刪除跳躍表中包含給定成員和分值的節點 | 平均O(logN),最壞O(N),N為跳躍表長度 |
zslGetRank | 返回包含給定成員和分值的節點在跳躍表中的排位 | 平均O(logN),最壞O(N),N為跳躍表長度 |
zslGetElementByRank | 返回包含給定成員和分值的節點在跳躍表中的排位 | 平均O(logN) ,最壞O(N),N為跳躍表長度 |
zslIsInRange | 給定一個分值範圍(range),比如0到15,20到28,諸如此類,如果給定的分值範圍包含在跳躍表的分值範圍內,返回1,否則返回0 | O(1),基於通過跳躍表的表頭節點和表尾節點的分值得到範圍 |
zslFirstInRange | 給定一個分值範圍,返回跳躍表中第一個符合這個範圍的節點 | 平均O(logN),最壞O(N),N為跳躍表長度 |
zslLastInRange | 給定一個分值範圍,返回跳躍表中最後一個符合這個範圍的節點 | 平均O(logN),最壞O(N),N為跳躍表長度 |
zslDeleteRangeByScore | 給定一個分值範圍,刪除跳躍表中所有在這個範圍之內的節點 | O(N),N為被刪除節點數量 |
zslDeleteRangeByRank | 給定一個排位範圍,刪除跳躍表中所有在這個範圍之內的節點 | O(N),N為被刪除節點數量 |
2. 整數集合
- 整數集合 intset,其特點是從小到大儲存整數且不會重複;
- 整數集合在Redis裡的應用:集合鍵的底層實現;
2.1 整數集合的實現
-
整數集合的定義,在
intset.h/intset
結構中:typedef struct intset{ //編碼方式 uint32_t encoding; //集合包含的元素數量 uint32_t length; //儲存元素的陣列 int8_t contents[]; } intset;
contents
宣告為 int8_t 型別的陣列,但陣列的真正型別取決於encoding
屬性的值;
encoding值 contents值 範圍 INTSET_ENC_INT16 int16_t -32768~32768 INTSET_ENC_INT32 int32_t -2147483648~2147483647 INTSET_ENC_INT64 int64_t -9223372036854775808~9223372036854775807
2.2 整數集合的型別升級
- 當新增的元素型別比整數集合現有元素的型別長時,需要升級;
- 步驟:
- 根據新元素型別,擴充套件整數集合底層陣列空間大小,併為新元素分配空間;
- 將底層陣列現有元素轉換成新元素相同的型別,在維持集合有序性質不變情況下將轉換後的元素放置到正確位置上;
- 將新元素新增到底層數組裡;
- 因為新增新元素可能會引起升級,每次升級需要對所有元素進行型別轉換,因此時間複雜度為O(N);
- 因為引起升級操作的新元素比現有元素長,所以新元素要麼新增到陣列開頭,要麼陣列末尾;
- 好處:
- 靈活性:C語言通常不會將不同型別值放在同一個資料結構裡,Redis的升級使其可以;
- 節約記憶體;
- 整數集合不允許降級操作;
2.3 整數集合的API
函式 | 作用 | 時間複雜度 |
---|---|---|
intsetNew | 建立一個新的整數集合 | O(1) |
intsetAdd | 將給定元素新增到整數集合裡面 | O(N) |
intsetRemove | 從整數集合中移除給定元素 | O(N) |
intsetFind | 檢查給定值是否存在於集合 | O(logN),整數集合有序排列,可以用二分查詢法 |
intsetRandom | 從整數集合中隨機返回一個元素 | O(1) |
intsetGet | 取出底層陣列在給定索引上的元素 | O(1) |
intsetLen | 返回整數集合包含的元素個數 | O(1) |
intsetBlobLen | 返回整數集合咱用的記憶體位元組數 | O(1) |
3. 壓縮列表
- 壓縮列表 ziplist,其特點是管理小整數值和短字串;
- 壓縮列表在Redis裡的應用:列表鍵與雜湊鍵的底層實現之一;
- 壓縮列表的Redis為節省記憶體而開發的,是由一系列特殊編碼的連續記憶體塊組成的順序型(sequential)資料結構;
3.1 壓縮列表的結構
- 壓縮列表是由一系列特殊編碼的連續記憶體塊組成的順序型資料結構;
3.2 壓縮列表節點的定義
-
節點的定義在
ziplist.c/zlentry
結構裡:typedef struct zlentry { // prevrawlen :前置節點的長度 // prevrawlensize :編碼 prevrawlen 所需的位元組大小 unsigned int prevrawlensize, prevrawlen; // len :當前節點值的長度 // lensize :編碼 len 所需的位元組大小 unsigned int lensize, len; // 當前節點 header 的大小 // 等於 prevrawlensize + lensize unsigned int headersize; // 當前節點值所使用的編碼型別 unsigned char encoding; // 指向當前節點的指標 unsigned char *p; } zlentry;
- 可以用當前節點地址減去
prevrawlen
的值獲得前置節點的首地址,可以由此實現從尾到頭的遍歷; *p
指向一個content
,儲存節點的值,值的型別和長度由encoding
決定;encoding
的屬性(下劃線表示留空,abcdx代表實際二進位制資料):
- 可以用當前節點地址減去
3.3 連鎖更新
- 首先,壓縮列表節點有個
prevrawlen
屬性,用於記錄前一個節點的長度,前一個節點的長度變化會影響prevrawlen
屬性的長度取值(使用1個位元組儲存前一個節點的長度還是5個); - 假設所有結點(e1, e2......eN)長度介於250253位元組之間,在表頭新增長度大於等於254位元組的new節點,因為e1的`prevrawlen`屬性僅1位元組,無法儲存大於254的數字(new的長度),因此需要擴充套件為5位元組長,此時e1的長度介於254257位元組之間。這樣,new引發e1的擴充套件,e1引發e2的擴充套件,形成連鎖更新;
- 刪除節點也可能引發連鎖更新;
- 連鎖更新的最壞時間複雜度為 O(N2);
- 在實際中,連鎖更新造成的效能問題機率很低;
3.4 壓縮列表的API
函式 | 作用 | 時間複雜度 |
---|---|---|
ziplistNew | 建立一個新的壓縮列表 | O(1) |
ziplistPush | 建立一個包含給定值的新節點,並將這個新節點新增到壓縮列表的表頭或表尾 | 平均O(N),最壞O(N2) |
ziplistInsert | 將包含給定值的新節點插入到給定節點之後 | 平均O(N),最壞O(N2) |
ziplistIndex | 返回壓縮列表給定索引上的節點 | O(N) |
ziplistFind | 在壓縮列表中查詢並返回包含了給定值的節點 | 當儲存的是位元組數字時為O(N2),整數時為O(N) |
ziplistNext | 返回給定節點的下一個節點 | O(1) |
ziplistPrev | 返回給定節點的前一個節點 | O(1) |
ziplistGet | 獲取給頂節點說儲存的值 | O(1) |
ziplistDelete | 從壓縮列表中刪除給定的節點 | 平均O(N),最壞O(N2) |
ziplistDeleteRange | 刪除壓縮列表在給定索引上的連續多個節點 | 平均O(N),最壞O(N2) |
ziplistBlobLen | 返回壓縮列表目前佔用的記憶體位元組數 | O(1) |
ziplistLen | 返回壓縮列表目前包含的節點數量 | 節點數量小於65535時為O(1),大於65535時為O(N) |
- 最壞時間複雜度為O(N2)是因為可能引發連鎖更新;