Redis學習筆記--Redis基本類型及其數據結構

什麼是Redis?

Redis(全稱:Remote Dictionary Server 遠程字典服務)是一個開源的使用ANSI C語言編寫、支持網絡、可基於內存亦可持久化的日誌型、Key-Value數據庫,並提供多種語言的API。它通常被稱爲數據結構服務器,因爲值(value)可以是 字符串(String), 哈希(Hash), 列表(list), 集合(sets) 和 有序集合(sorted sets)等類型。

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 string

    SDS

    SDS的內部數據結構:

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

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

    因爲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內部有很多層L1、L2等,指針指向這一層的下一個結點。BW是回退指針(backward),用於查找的時候回退。然後下面是score和對象本身object。

總結

Redis一共分爲5中基本數據類型:String,Hash,List,Set,ZSet.Redis對外暴露的是對象(數據類型),而每個對象都是用一個redisObject持有,通過不同的編碼,映射到不同的數據結構。從最開始的那個圖可以知道,有時候不同對象可能會底層使用同一種數據結構,比如壓縮列表和字典等。

在瞭解數據結構後,我們就能夠更清楚應該選用什麼樣的對象,出現問題時應該如何優化了。

參考文章

 本文主要參考了博客主“崖邊小生”的Redis系列文章。博客鏈接:https://www.cnblogs.com/hunternet/tag/Redis/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章