面試中,redis也是很受面試官親睞的一部分。我向在這裏講的是redis的底層數據結構,而不是你理解的五大數據結構。你有沒有想過redis底層是怎樣的數據結構呢,他們和咱們java中的HashMap、List、等使用的數據結構有什麼區別呢。java
咱們都知道redis是用C語言寫,可是C語言處理字符串和數組的成本是很高的,下面我分別說幾個例子。node
strcat()
,在用這個函數以前必需要先給目標變量分配足夠的空間,不然就會溢出。好了,Redis本身構建了一種名叫Simple dynamic string(SDS)
的數據結構,他分別對這幾個問題做了處理。咱們先來看看它的結構源碼:git
struct sdshdr{
//記錄buf數組中已使用字節的數量
//等於 SDS 保存字符串的長度
int len;
//記錄 buf 數組中未使用字節的數量
int free;
//字節數組,用於保存字符串
char buf[];
}
複製代碼
再來講說它的優勢:github
len字段
。free字段
,會默認留夠必定的空間防止屢次重分配內存。更多瞭解:redis.io/topics/inte…面試
這就是string的底層實現,更是redis對全部字符串數據的處理方式(SDS會被嵌套到別的數據結構裏使用)。redis
Redis的鏈表在雙向鏈表上擴展了頭、尾節點、元素數等屬性。算法
ListNode節點數據結構:segmentfault
typedef struct listNode{
//前置節點
struct listNode *prev;
//後置節點
struct listNode *next;
//節點的值
void *value;
}listNode
複製代碼
鏈表數據結構:數組
typedef struct list{
//表頭節點
listNode *head;
//表尾節點
listNode *tail;
//鏈表所包含的節點數量
unsigned long len;
//節點值複製函數
void (*free) (void *ptr);
//節點值釋放函數
void (*free) (void *ptr);
//節點值對比函數
int (*match) (void *ptr,void *key);
}list;
複製代碼
從上面能夠看到,Redis的鏈表有這幾個特色:bash
Redis的Hash,就是在
數組+鏈表
的基礎上,進行了一些rehash優化等。
哈希表:
typedef struct dictht {
// 哈希表數組
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩碼,用於計算索引值
// 老是等於 size - 1
unsigned long sizemask;
// 該哈希表已有節點的數量
unsigned long used;
} dictht;
複製代碼
Hash表節點:
typedef struct dictEntry {
// 鍵
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// 指向下個哈希表節點,造成鏈表
struct dictEntry *next; // 單鏈表結構
} dictEntry;
複製代碼
字典:
typedef struct dict {
// 類型特定函數
dictType *type;
// 私有數據
void *privdata;
// 哈希表
dictht ht[2];
// rehash 索引
// 當 rehash 不在進行時,值爲 -1
int rehashidx; /* rehashing not in progress if rehashidx == -1 */
} dict;
複製代碼
能夠看出:
下面咱們講一下它的rehash優化。
當哈希表的鍵對泰國或者太少,就須要對哈希表的大小進行調整,redis是如何調整的呢?
dict
結構裏有個字段dictht ht[2]
表明有兩個dictht數組。第一步就是爲ht[1]哈希表分配空間,大小取決於ht[0]當前使用的狀況。咱們在3.2中看到,redis處理rehash的流程,可是更細一點的講,它如何進行數據遷的呢?
這就涉及到了漸進式rehash,redis考慮到大量數據遷移帶來的cpu繁忙(可能致使一段時間內中止服務),因此採用了漸進式rehash的方案。步驟以下:
rehashidx++
(注意:ht[0]中的數據是隻減不增的)。它的好處:採用分而治之的思想,將龐大的遷移工做量劃分到每一次CURD中,避免了服務繁忙。
這個數據結構是我面試中見過最多的,它其實特別簡單。學過的人可能都知道,它和平衡樹性能很類似,但爲何不用平衡樹而用skipList呢?
能夠看到,skipList中的元素是有序的,因此跳躍表在redis中用在有序集合鍵、集羣節點內部數據結構
跳躍表節點:
typedef struct zskiplistNode {
// 後退指針
struct zskiplistNode *backward;
// 分值
double score;
// 成員對象
robj *obj;
// 層
struct zskiplistLevel {
// 前進指針
struct zskiplistNode *forward;
// 跨度
unsigned int span;
} level[];
} zskiplistNode;
複製代碼
跳躍表:
typedef struct zskiplist {
// 表頭節點和表尾節點
struct zskiplistNode *header, *tail;
// 表中節點的數量
unsigned long length;
// 表中層數最大的節點的層數
int level;
} zskiplist;
複製代碼
它有幾個概念:
層,也就是level[]
字段,層的數量越多,訪問節點速度越快。(由於它至關因而索引,層數越多,它索引就越細,就能很快找到索引值)
層中有一個forward
字段,用於從表頭向表尾方向訪問。
用於記錄兩個節點之間的距離
用於從表尾向表頭方向訪問。
level0 1---------->5
level1 1---->3---->5
level2 1->2->3->4->5->6->7->8
複製代碼
好比我要找鍵爲6的元素,在level0中直接定位到5,而後再日後走一個元素就找到了。
Reids對整數存儲專門做了優化,intset就是redis用於保存整數值的集合數據結構。當一個結合中只包含整數元素,redis就會用這個來存儲。
127.0.0.1:6379[2]> sadd number 1 2 3 4 5 6
(integer) 6
127.0.0.1:6379[2]> object encoding number
"intset"
複製代碼
intset數據結構:
typedef struct intset {
// 編碼方式
uint32_t encoding;
// 集合包含的元素數量
uint32_t length;
// 保存元素的數組
int8_t contents[];
} intset;
複製代碼
你確定很好奇編碼方式(encoding)字段是幹嗎用的呢?
說白了就是根據contents字段來判斷用哪一個int類型更好,也就是對int存儲做了優化。
說到優化,那redis如何做的呢?就涉及到了升級。
若是咱們有個Int16類型的整數集合,如今要將65535(int32)加進這個集合,int16是存儲不下的,因此就要對整數集合進行升級。
假如如今有2個int16的元素:1和2,新加入1個int32位的元素65535。
按照上面的例子,若是我把65535又刪掉,encoding會不會又回到Int16呢,答案是不會的。官方沒有給出理由,我以爲應該是下降性能消耗吧,畢竟調整一次是O(N)的時間複雜度。
ziplist是redis爲了節約內存而開發的順序型數據結構。它被用在列表鍵和哈希鍵中。通常用於小數據存儲。
引用https://segmentfault.com/a/1190000016901154中的兩個圖:
ziplist沒有明肯定義結構體,這裏只做大概的演示。
typedef struct entry {
/*前一個元素長度須要空間和前一個元素長度*/
unsigned int prevlengh;
/*元素內容編碼*/
unsigned char encoding;
/*元素實際內容*/
unsigned char *data;
}zlentry;
複製代碼
typedef struct ziplist{
/*ziplist分配的內存大小*/
uint32_t zlbytes;
/*達到尾部的偏移量*/
uint32_t zltail;
/*存儲元素實體個數*/
uint16_t zllen;
/*存儲內容實體元素*/
unsigned char* entry[];
/*尾部標識*/
unsigned char zlend;
}ziplist;
複製代碼
第一次看可能會特別矇蔽,你細細的把我這段話看完就必定能懂。
entry結構體裏面有三個重要的字段:
上面有說到,previous_entry_length這個字段存放上個節點的長度,那默認長度給分配多少呢?redis是這樣分的,若是前節點長度小於254,就分配1字節,大於的話分配5字節,那問題就來了。
若是前一個節點的長度剛開始小於254字節,後來大於254,那不就存放不下了嗎? 這就涉及到previous_entry_length的更新,可是改一個確定不行阿,後面的節點內存信息都須要改。因此就須要從新分配內存,而後連鎖更新包括該受影響節點後面的全部節點。
除了增長新節點會引起連鎖更新、刪除節點也會觸發。
一個由ziplist組成的雙向鏈表。可是一個quicklist能夠有多個quicklist節點,它很像B樹的存儲方式。是在redis3.2版本中新加的數據結構,用在列表的底層實現。
表頭結構:
typedef struct quicklist {
//指向頭部(最左邊)quicklist節點的指針
quicklistNode *head;
//指向尾部(最右邊)quicklist節點的指針
quicklistNode *tail;
//ziplist中的entry節點計數器
unsigned long count; /* total count of all entries in all ziplists */
//quicklist的quicklistNode節點計數器
unsigned int len; /* number of quicklistNodes */
//保存ziplist的大小,配置文件設定,佔16bits
int fill : 16; /* fill factor for individual nodes */
//保存壓縮程度值,配置文件設定,佔16bits,0表示不壓縮
unsigned int compress : 16; /* depth of end nodes not to compress;0=off */
} quicklist;
複製代碼
quicklist節點結構:
typedef struct quicklistNode {
struct quicklistNode *prev; //前驅節點指針
struct quicklistNode *next; //後繼節點指針
//不設置壓縮數據參數recompress時指向一個ziplist結構
//設置壓縮數據參數recompress指向quicklistLZF結構
unsigned char *zl;
//壓縮列表ziplist的總長度
unsigned int sz; /* ziplist size in bytes */
//ziplist中包的節點數,佔16 bits長度
unsigned int count : 16; /* count of items in ziplist */
//表示是否採用了LZF壓縮算法壓縮quicklist節點,1表示壓縮過,2表示沒壓縮,佔2 bits長度
unsigned int encoding : 2; /* RAW==1 or LZF==2 */
//表示一個quicklistNode節點是否採用ziplist結構保存數據,2表示壓縮了,1表示沒壓縮,默認是2,佔2bits長度
unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */
//標記quicklist節點的ziplist以前是否被解壓縮過,佔1bit長度
//若是recompress爲1,則等待被再次壓縮
unsigned int recompress : 1; /* was this node previous compressed? */
//測試時使用
unsigned int attempted_compress : 1; /* node can't compress; too small */ //額外擴展位,佔10bits長度 unsigned int extra : 10; /* more bits to steal for future usage */ } quicklistNode; 複製代碼
在redis.conf中的ADVANCED CONFIG部分:
list-max-ziplist-size -2
list-compress-depth 0
複製代碼
咱們來詳細解釋一下list-max-ziplist-size
這個參數的含義。它能夠取正值,也能夠取負值。
當取正值的時候,表示按照數據項個數來限定每一個quicklist節點上的ziplist長度。好比,當這個參數配置成5的時候,表示每一個quicklist節點的ziplist最多包含5個數據項。
當取負值的時候,表示按照佔用字節數來限定每一個quicklist節點上的ziplist長度。這時,它只能取-1到-5這五個值,每一個值含義以下:
-5: 每一個quicklist節點上的ziplist大小不能超過64 Kb。(注:1kb => 1024 bytes)
-4: 每一個quicklist節點上的ziplist大小不能超過32 Kb。
-3: 每一個quicklist節點上的ziplist大小不能超過16 Kb。
-2: 每一個quicklist節點上的ziplist大小不能超過8 Kb。(-2是Redis給出的默認值)
這個參數表示一個quicklist兩端不被壓縮的節點個數。注:這裏的節點個數是指quicklist雙向鏈表的節點個數,而不是指ziplist裏面的數據項個數。實際上,一個quicklist節點上的ziplist,若是被壓縮,就是總體被壓縮的。
參數list-compress-depth的取值含義以下:
0: 是個特殊值,表示都不壓縮。這是Redis的默認值。 1: 表示quicklist兩端各有1個節點不壓縮,中間的節點壓縮。 2: 表示quicklist兩端各有2個節點不壓縮,中間的節點壓縮。 3: 表示quicklist兩端各有3個節點不壓縮,中間的節點壓縮。 依此類推…
Redis對於quicklist內部節點的壓縮算法,採用的LZF——一種無損壓縮算法。
關注個人公衆號,隨時閱讀個人所有文章。
想看往期文章, 請點擊個人GitHub地址: github.com/fantj2016/j…