Redis基本類型及其數據結構

認真寫文章,用心作分享。redis

我的網站:yasinshaw.com數組

公衆號:xy的技術圈數據結構

之前在使用Redis的時候,只是簡單地使用它提供的基本數據類型和接口,並無深刻研究它底層的數據結構。最近打算從新學習梳理一下Redis方面的知識,因此打算從介紹Redis的基本類型及其數據結構入手。函數

redisObject

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;
複製代碼

簡單介紹一下這幾個字段:優化

  • type:數據類型,就是咱們熟悉的string、hash、list等。
  • encoding:內部編碼,其實就是本文要介紹的數據結構。指的是當前這個value底層是用的什麼數據結構。由於同一個數據類型底層也有多種數據結構的實現,因此這裏須要指定數據結構。
  • REDIS_LRU_BITS:當前對象能夠保留的時長。這個咱們在後面講鍵的過時策略的時候講。
  • refcount:對象引用計數,用於GC。
  • ptr:指針,指向以encoding的方式實現這個對象的實際地址。

數據類型及其數據結構

string

在Redis內部,string類型有兩種底層儲存結構。Redis會根據存儲的數據及用戶的操做指令自動選擇合適的結構:網站

  • int:存放整數類型;
  • SDS:存放浮點、字符串、字節類型;

SDS: 簡單動態字符串 simple dynamic stringui

SDS

SDS的內部數據結構:編碼

typedef struct sdshdr {
    // buf中已經佔用的字符長度
    unsigned int len;
    // buf中剩餘可用的字符長度
    unsigned int free;
    // 數據空間
    char buf[];
}
複製代碼

可見,其底層是一個char數組。buf最大容量爲512M,裏面能夠放字符串、浮點數和字節。因此你甚至能夠放一張序列化後的圖片。它爲何沒有直接使用數組,而是包裝成了這樣的數據結構呢?spa

由於buf會有動態擴容和縮容的需求。若是直接使用數組,那每次對字符串的修改都會致使從新分配內存,效率很低。

buf的擴容過程以下:

  • 若是修改後len長度將小於1M,這時分配給free的大小和len同樣,例如修改事後爲10字節, 那麼給free也是10字節,buf實際長度變成了10 + 10 + 1 = 21byte
  • 若是修改後len長度將大於等於1M,這時分配給free的長度爲1M,例如修改事後爲30M,那麼給free是1M.buf實際長度變成了30M + 1M + 1byte

擴容

惰性空間釋放指的是當字符串縮短時,並無真正的縮容,而是移動free的指針。這樣未來字符串長度增長時,就不用從新分配內存了。但這樣會形成內存浪費,Redis提供了API來真正釋放內存。

list

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屬性來記錄這個節點的長度,這樣比較方便地獲得下一個節點的位置。

壓縮列表

上圖的各字段含義爲:

  • zlbytes:列表的總長度
  • zltail:指向最末元素
  • zllen:元素的個數
  • entry:元素的內容,裏面記錄了前一個Entry的長度,用於方便雙向遍歷
  • zlend:恆爲0xFF,做爲ziplist的定界符

壓縮列表不僅是list的底層實現,也是hash的底層實現之一。當hash的元素個數少且內容長度不大時,使用壓縮列表來實現。

hash

hash底層有兩種實現:壓縮列表和字典(dict)。壓縮列表剛剛上面已經介紹過了,下面主要介紹一下字典的數據結構。

字典

字典其實就相似於Java語言中的Map,Python語言中的dict。與Java中的HashMap相似,Redis底層也是使用的散列表做爲字典的實現,解決hash衝突使用的是鏈表法。Redis一樣使用了一個數據結構來持有這個散列表:

鏈表

在鍵增長或減小時,會擴容或縮容,而且進行rehash,根據hash值從新計算索引值。那若是這個字典太大了怎麼辦呢?

爲了解決一次性擴容耗時過多的狀況,能夠將擴容操做穿插在插入操做的過程當中,分批完成。當負載因子觸達閾值以後,只申請新空間,但並不將老的數據搬移到新散列表中。當有新數據要插入時,將新數據插入新散列表中,而且從老的散列表中拿出一個數據放入到新散列表。每次插入一個數據到散列表,都重複上面的過程。通過屢次插入操做以後,老的散列表中的數據就一點一點所有搬移到新散列表中了。這樣沒有了集中的一次一次性數據搬移,插入操做就都變得很快了。這個過程也被稱爲漸進式rehash

set

set裏面沒有重複的集合。set的實現比較簡單。若是是整數類型,就直接使用整數集合intset。使用二分查找來輔助,速度仍是挺快的。不過在插入的時候,因爲要移動元素,時間複雜度是O(N)。

若是不是整數類型,就使用上面在hash那一節介紹的字典。key爲set的值,value爲空

zset

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系列文章,感謝做者。博客連接:

www.cnblogs.com/hunternet/t…

關注公衆號:xy的技術圈

相關文章
相關標籤/搜索