認真寫文章,用心作分享。redis
我的網站:yasinshaw.com數組
公衆號:xy的技術圈數據結構
之前在使用Redis的時候,只是簡單地使用它提供的基本數據類型和接口,並無深刻研究它底層的數據結構。最近打算從新學習梳理一下Redis方面的知識,因此打算從介紹Redis的基本類型及其數據結構入手。函數
Redis的key是頂層模型,它的value是扁平化的。Redis中,全部的value都是一個object,它的結構以下:學習
typedef struct redisObject {
unsigned [type] 4;
unsigned [encoding] 4;
unsigned [lru] REDIS_LRU_BITS;
int refcount;
void *ptr;
} robj;
複製代碼
簡單介紹一下這幾個字段:優化
在Redis內部,string類型有兩種底層儲存結構。Redis會根據存儲的數據及用戶的操做指令自動選擇合適的結構:網站
SDS: 簡單動態字符串 simple dynamic stringui
SDS的內部數據結構:編碼
typedef struct sdshdr {
// buf中已經佔用的字符長度
unsigned int len;
// buf中剩餘可用的字符長度
unsigned int free;
// 數據空間
char buf[];
}
複製代碼
可見,其底層是一個char數組。buf最大容量爲512M,裏面能夠放字符串、浮點數和字節。因此你甚至能夠放一張序列化後的圖片。它爲何沒有直接使用數組,而是包裝成了這樣的數據結構呢?spa
由於buf會有動態擴容和縮容的需求。若是直接使用數組,那每次對字符串的修改都會致使從新分配內存,效率很低。
buf的擴容過程以下:
惰性空間釋放指的是當字符串縮短時,並無真正的縮容,而是移動free的指針。這樣未來字符串長度增長時,就不用從新分配內存了。但這樣會形成內存浪費,Redis提供了API來真正釋放內存。
list底層有兩種數據結構:鏈表linkedlist和壓縮列表ziplist。當list元素個數少且元素內容長度不大時,使用ziplist實現,不然使用linkedlist。
Redis使用的鏈表是雙向鏈表。爲了方便操做,使用了一個list結構來持有這個鏈表。如圖所示:
typedef struct list{
//表頭節點
listNode *head;
//表尾節點
listNode *tail;
//鏈表所包含的節點數量
unsigned long len;
//節點值複製函數
void *(*dup)(void *ptr);
//節點值釋放函數
void *(*free)(void *ptr);
//節點值對比函數
int (*match)(void *ptr,void *key);
}list;
複製代碼
data存的其實也是一個指針。鏈表裏面的元素是上面介紹的string。由於是雙向鏈表,因此能夠很方便地把它當成一個棧或者隊列來使用。
與上面的鏈表相對應,壓縮列表有點兒相似數組,經過一片連續的內存空間,來存儲數據。不過,它跟數組不一樣的一點是,它容許存儲的數據大小不一樣。每一個節點上增長一個length屬性來記錄這個節點的長度,這樣比較方便地獲得下一個節點的位置。
上圖的各字段含義爲:
壓縮列表不僅是list的底層實現,也是hash的底層實現之一。當hash的元素個數少且內容長度不大時,使用壓縮列表來實現。
hash底層有兩種實現:壓縮列表和字典(dict)。壓縮列表剛剛上面已經介紹過了,下面主要介紹一下字典的數據結構。
字典其實就相似於Java語言中的Map
,Python語言中的dict
。與Java中的HashMap
相似,Redis底層也是使用的散列表做爲字典的實現,解決hash衝突使用的是鏈表法。Redis一樣使用了一個數據結構來持有這個散列表:
在鍵增長或減小時,會擴容或縮容,而且進行rehash,根據hash值從新計算索引值。那若是這個字典太大了怎麼辦呢?
爲了解決一次性擴容耗時過多的狀況,能夠將擴容操做穿插在插入操做的過程當中,分批完成。當負載因子觸達閾值以後,只申請新空間,但並不將老的數據搬移到新散列表中。當有新數據要插入時,將新數據插入新散列表中,而且從老的散列表中拿出一個數據放入到新散列表。每次插入一個數據到散列表,都重複上面的過程。通過屢次插入操做以後,老的散列表中的數據就一點一點所有搬移到新散列表中了。這樣沒有了集中的一次一次性數據搬移,插入操做就都變得很快了。這個過程也被稱爲漸進式rehash。
set裏面沒有重複的集合。set的實現比較簡單。若是是整數類型,就直接使用整數集合intset。使用二分查找來輔助,速度仍是挺快的。不過在插入的時候,因爲要移動元素,時間複雜度是O(N)。
若是不是整數類型,就使用上面在hash那一節介紹的字典。key爲set的值,value爲空。
zset是可排序的set。與hash的實現方式相似,若是元素個數很少且不大,就使用壓縮列表ziplist來存儲。不過因爲zset包含了score的排序信息,因此在ziplist內部,是按照score排序遞增來存儲的。意味着每次插入數據都要移動以後的數據。
跳錶(skiplist)是另外一種實現dict的數據結構。跳錶是對鏈表的一個加強。咱們在使用鏈表的時候,即便元素的有序排列的,但若是要查找一個元素,也須要從頭一個個查找下去,時間複雜度是O(N)。而跳錶顧名思義,就是跳躍了一些元素,能夠抽象多層。
以下圖所示,好比咱們要查找8,先在最上層L2查找,發如今1和9之間;而後去L1層查找,發如今5和9之間;而後去L0查找,發如今7和9之間,而後找到8。
當元素比較多時,使用跳錶能夠顯著減小查找的次數。
同list相似,Redis內部也不是直接使用的跳錶,而是使用了一個自定義的數據結構來持有跳錶。下圖左邊藍色部分是skiplist,右邊是4個zskiplistNode。zskiplistNode內部有不少層L一、L2等,指針指向這一層的下一個結點。BW是回退指針(backward),用於查找的時候回退。而後下面是score和對象自己object。
Redis對外暴露的是對象(數據類型),而每一個對象都是用一個redisObject持有,經過不一樣的編碼,映射到不一樣的數據結構。從最開始的那個圖能夠知道,有時候不一樣對象可能會底層使用同一種數據結構,好比壓縮列表和字典等。
在瞭解數據結構後,咱們就可以更清楚應該選用什麼樣的對象,出現問題時應該如何優化了。
本文主要參考了博客主「崖邊小生」的Redis系列文章,感謝做者。博客連接:
關注公衆號:xy的技術圈