<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          一文讀懂Redis常見(jiàn)對(duì)象類型的底層數(shù)據(jù)結(jié)構(gòu)

          共 12756字,需瀏覽 26分鐘

           ·

          2020-11-23 11:43

          點(diǎn)擊上方 好好學(xué)java ,選擇 星標(biāo) 公眾號(hào)

                     
          重磅資訊、干貨,第一時(shí)間送達(dá)

          今日推薦:硬剛一周,3W字總結(jié),一年的經(jīng)驗(yàn)告訴你如何準(zhǔn)備校招!

                    
          個(gè)人原創(chuàng)100W+訪問(wèn)量博客:點(diǎn)擊前往,查看更多

          轉(zhuǎn)自:伍陸七,

          鏈接:cnblogs.com/chentianming/p/13838347.html

          Redis 是一個(gè)基于內(nèi)存中的數(shù)據(jù)結(jié)構(gòu)存儲(chǔ)系統(tǒng),可以用作數(shù)據(jù)庫(kù)、緩存和消息中間件。Redis 支持五種常見(jiàn)對(duì)象類型:字符串(String)、哈希(Hash)、列表(List)、集合(Set)以及有序集合(Zset),我們?cè)谌粘9ぷ髦幸矔?huì)經(jīng)常使用它們。知其然,更要知其所以然,本文將會(huì)帶你讀懂這五種常見(jiàn)對(duì)象類型的底層數(shù)據(jù)結(jié)構(gòu)。


          本文主要內(nèi)容參考自《Redis設(shè)計(jì)與實(shí)現(xiàn)》


          1. 對(duì)象類型和編碼


          Redis 使用對(duì)象來(lái)存儲(chǔ)鍵和值的,在Redis中,每個(gè)對(duì)象都由 redisObject 結(jié)構(gòu)表示。redisObject 結(jié)構(gòu)主要包含三個(gè)屬性:type、encoding 和 ptr。


          typedef struct redisObject {    // 類型    unsigned type:4;    // 編碼    unsigned encoding:4;    // 底層數(shù)據(jù)結(jié)構(gòu)的指針    void *ptr;} robj;


          其中 type 屬性記錄了對(duì)象的類型。對(duì)于 Redis 來(lái)說(shuō),鍵對(duì)象總是字符串類型,值對(duì)象可以是任意支持的類型。因此,當(dāng)我們說(shuō) Redis 鍵采用哪種對(duì)象類型的時(shí)候,指的是對(duì)應(yīng)的值采用哪種對(duì)象類型。



          *ptr 屬性指向了對(duì)象的底層數(shù)據(jù)結(jié)構(gòu),而這些數(shù)據(jù)結(jié)構(gòu)由 encoding 屬性決定。



          之所以由 encoding 屬性來(lái)決定對(duì)象的底層數(shù)據(jù)結(jié)構(gòu),是為了實(shí)現(xiàn)同一對(duì)象類型,支持不同的底層實(shí)現(xiàn)。這樣就能在不同場(chǎng)景下,使用不同的底層數(shù)據(jù)結(jié)構(gòu),進(jìn)而極大提升Redis的靈活性和效率。


          底層數(shù)據(jù)結(jié)構(gòu)后面會(huì)詳細(xì)講解,這里簡(jiǎn)單看一下即可。


          2. 字符串對(duì)象


          字符串是我們?nèi)粘9ぷ髦杏玫米疃嗟膶?duì)象類型,它對(duì)應(yīng)的編碼可以是 int、raw 和 embstr。字符串對(duì)象相關(guān)命令可參考:Redis命令-Strings。


          如果一個(gè)字符串對(duì)象保存的是不超過(guò) long 類型的整數(shù)值,此時(shí)編碼類型即為 int,其底層數(shù)據(jù)結(jié)構(gòu)直接就是 long 類型。例如執(zhí)行 set number 10086,就會(huì)創(chuàng)建 int 編碼的字符串對(duì)象作為 number 鍵的值。



          如果字符串對(duì)象保存的是一個(gè)長(zhǎng)度大于 39 字節(jié)的字符串,此時(shí)編碼類型即為 raw,其底層數(shù)據(jù)結(jié)構(gòu)是簡(jiǎn)單動(dòng)態(tài)字符串(SDS);如果長(zhǎng)度小于等于 39 個(gè)字節(jié),編碼類型則為 embstr,底層數(shù)據(jù)結(jié)構(gòu)就是 embstr 編碼 SDS。下面,我們?cè)敿?xì)理解下什么是簡(jiǎn)單動(dòng)態(tài)字符串。


          2.1 簡(jiǎn)單動(dòng)態(tài)字符串


          SDS 定義


          在 Redis 中,使用 sdshdr 數(shù)據(jù)結(jié)構(gòu)表示 SDS:


          struct sdshdr {    // 字符串長(zhǎng)度    int len;    // buf數(shù)組中未使用的字節(jié)數(shù)    int free;    // 字節(jié)數(shù)組,用于保存字符串    char buf[];};


          SDS 遵循了 C 字符串以空字符結(jié)尾的慣例,保存空字符的 1 字節(jié)不會(huì)計(jì)算在 len 屬性里面。例如,Redis 這個(gè)字符串在 SDS 里面的數(shù)據(jù)可能是如下形式:


          SDS 與 C 字符串的區(qū)別


          C 語(yǔ)言使用長(zhǎng)度為 N+1 的字符數(shù)組來(lái)表示長(zhǎng)度為N的字符串,并且字符串的最后一個(gè)元素是空字符 \0。Redis 采用 SDS 相對(duì)于 C 字符串有如下幾個(gè)優(yōu)勢(shì):


          1. 常數(shù)復(fù)雜度獲取字符串長(zhǎng)度;

          2. 杜絕緩沖區(qū)溢出;

          3. 減少修改字符串時(shí)帶來(lái)的內(nèi)存重分配次數(shù);

          4. 二進(jìn)制安全。


          常數(shù)復(fù)雜度獲取字符串長(zhǎng)度


          因?yàn)?C 字符串并不記錄自身的長(zhǎng)度信息,所以為了獲取字符串的長(zhǎng)度,必須遍歷整個(gè)字符串,時(shí)間復(fù)雜度是 O(N)。而 SDS 使用 len 屬性記錄了字符串的長(zhǎng)度,因此獲取 SDS字符串長(zhǎng)度的時(shí)間復(fù)雜度是 O(1)


          杜絕緩沖區(qū)溢出


          C 字符串不記錄自身長(zhǎng)度帶來(lái)的另一個(gè)問(wèn)題是,很容易造成緩存區(qū)溢出。比如使用字符串拼接函數(shù)(stract)的時(shí)候,很容易覆蓋掉字符數(shù)組原有的數(shù)據(jù)。


          與 C 字符串不同,SDS 的空間分配策略完全杜絕了發(fā)生緩存區(qū)溢出的可能性。當(dāng) SDS 進(jìn)行字符串?dāng)U充時(shí),首先會(huì)檢查當(dāng)前的字節(jié)數(shù)組的長(zhǎng)度是否足夠。如果不夠的話,會(huì)先進(jìn)行自動(dòng)擴(kuò)容,然后再進(jìn)行字符串操作。


          減少修改字符串時(shí)帶來(lái)的內(nèi)存重分配次數(shù)


          因?yàn)?C 字符串的長(zhǎng)度和底層數(shù)據(jù)是緊密關(guān)聯(lián)的,所以每次增長(zhǎng)或者縮短一個(gè)字符串,程序都要對(duì)這個(gè)數(shù)組進(jìn)行一次內(nèi)存重分配:


          • 如果是增長(zhǎng)字符串操作,需要先通過(guò)內(nèi)存重分配來(lái)擴(kuò)展底層數(shù)組空間大小,不這么做就導(dǎo)致緩存區(qū)溢出;

          • 如果是縮短字符串操作,需要先通過(guò)內(nèi)存重分配來(lái)來(lái)回收不再使用的空間,不這么做就導(dǎo)致內(nèi)存泄漏。


          因?yàn)閮?nèi)存重分配涉及復(fù)雜的算法,并且可能需要執(zhí)行系統(tǒng)調(diào)用,所以通常是個(gè)比較耗時(shí)的操作。對(duì)于 Redis 來(lái)說(shuō),字符串修改是一個(gè)十分頻繁的操作。如果每次都像 C 字符串那樣進(jìn)行內(nèi)存重分配,對(duì)性能影響太大了,顯然是無(wú)法接受的。


          SDS 通過(guò)空閑空間解除了字符串長(zhǎng)度和底層數(shù)據(jù)之間的關(guān)聯(lián)。在 SDS 中,數(shù)組中可以包含未使用的字節(jié),這些字節(jié)數(shù)量由 free 屬性記錄。通過(guò)空閑空間,SDS 實(shí)現(xiàn)了空間預(yù)分配和惰性空間釋放兩種優(yōu)化策略


          1. 空間預(yù)分配


          空間預(yù)分配是用于優(yōu)化 SDS 字符串增長(zhǎng)操作的,簡(jiǎn)單來(lái)說(shuō)就是當(dāng)字節(jié)數(shù)組空間不足觸發(fā)重分配的時(shí)候,總是會(huì)預(yù)留一部分空閑空間。這樣的話,就能減少連續(xù)執(zhí)行字符串增長(zhǎng)操作時(shí)的內(nèi)存重分配次數(shù)。


          有兩種預(yù)分配的策略:


          • len 小于 1MB 時(shí):每次重分配時(shí)會(huì)多分配同樣大小的空閑空間;

          • len 大于等于 1MB 時(shí):每次重分配時(shí)會(huì)多分配 1MB 大小的空閑空間。


          2. 惰性空間釋放


          惰性空間釋放是用于優(yōu)化 SDS 字符串縮短操作的。簡(jiǎn)單來(lái)說(shuō)就是當(dāng)字符串縮短時(shí),并不立即使用內(nèi)存重分配來(lái)回收多出來(lái)的字節(jié),而是用 free 屬性記錄,等待將來(lái)使用。SDS 也提供直接釋放未使用空間的 API,在需要的時(shí)候,也能真正的釋放掉多余的空間。


          二進(jìn)制安全


          C 字符串中的字符必須符合某種編碼,并且除了字符串末尾之外,其它位置不允許出現(xiàn)空字符。這些限制使得 C 字符串只能保存文本數(shù)據(jù)。


          但是對(duì)于 Redis 來(lái)說(shuō),不僅僅需要保存文本,還要支持保存二進(jìn)制數(shù)據(jù)。為了實(shí)現(xiàn)這一目標(biāo),SDS 的 API 全部做到了二進(jìn)制安全(binary-safe)。


          2.2 raw 和 embstr 編碼的 SDS 區(qū)別


          我們?cè)谇懊嬷v過(guò),長(zhǎng)度大于 39 字節(jié)的字符串,編碼類型為 raw,底層數(shù)據(jù)結(jié)構(gòu)是簡(jiǎn)單動(dòng)態(tài)字符串(SDS)。這個(gè)很好理解,比如當(dāng)我們執(zhí)行 set story "Long, long, long ago there lived a king ..."(長(zhǎng)度大于39)之后,Redis 就會(huì)創(chuàng)建一個(gè) raw 編碼的 String 對(duì)象。


          數(shù)據(jù)結(jié)構(gòu)如下:



          長(zhǎng)度小于等于 39 個(gè)字節(jié)的字符串,編碼類型為 embstr,底層數(shù)據(jù)結(jié)構(gòu)則是 embstr 編碼 SDS。embstr 編碼是專門(mén)用來(lái)保存短字符串的,它和 raw 編碼最大的不同在于:raw 編碼會(huì)調(diào)用兩次內(nèi)存分配分別創(chuàng)建 redisObject 結(jié)構(gòu)和 sdshdr 結(jié)構(gòu);而 embstr 編碼則是只調(diào)用一次內(nèi)存分配,在一塊連續(xù)的空間上同時(shí)包含 redisObject 結(jié)構(gòu)和 sdshdr 結(jié)構(gòu)。


          2.3 編碼轉(zhuǎn)換


          int 編碼和 embstr 編碼的字符串對(duì)象在條件滿足的情況下會(huì)自動(dòng)轉(zhuǎn)換為 raw 編碼的字符串對(duì)象。


          對(duì)于 int 編碼來(lái)說(shuō),當(dāng)我們修改這個(gè)字符串為不再是整數(shù)值的時(shí)候,此時(shí)字符串對(duì)象的編碼就會(huì)從 int 變?yōu)?raw。


          對(duì)于 embstr 編碼來(lái)說(shuō),只要我們修改了字符串的值,此時(shí)字符串對(duì)象的編碼就會(huì)從 embstr 變?yōu)?raw。


          embstr 編碼的字符串對(duì)象可以認(rèn)為是只讀的,因?yàn)?Redis 為其編寫(xiě)任何修改程序。當(dāng)我們要修改 embstr 編碼字符串時(shí),都是先將轉(zhuǎn)換為 raw 編碼,然后再進(jìn)行修改。


          3. 列表對(duì)象


          列表對(duì)象的編碼可以是 linkedlist 或者 ziplist,對(duì)應(yīng)的底層數(shù)據(jù)結(jié)構(gòu)是鏈表和壓縮列表。列表對(duì)象相關(guān)命令可參考:Redis 命令-List


          默認(rèn)情況下,當(dāng)列表對(duì)象保存的所有字符串元素的長(zhǎng)度都小于 64 字節(jié),且元素個(gè)數(shù)小于 512 個(gè)時(shí),列表對(duì)象采用的是 ziplist 編碼,否則使用 linkedlist 編碼。


          可以通過(guò)配置文件修改該上限值。


          3.1 鏈表


          鏈表是一種非常常見(jiàn)的數(shù)據(jù)結(jié)構(gòu),提供了高效的節(jié)點(diǎn)重排能力以及順序性的節(jié)點(diǎn)訪問(wèn)方式。在 Redis 中,每個(gè)鏈表節(jié)點(diǎn)使用 listNode 結(jié)構(gòu)表示:


          typedef struct listNode {    // 前置節(jié)點(diǎn)    struct listNode *prev;    // 后置節(jié)點(diǎn)    struct listNode *next;    // 節(jié)點(diǎn)值    void *value;} listNode


          多個(gè) listNode 通過(guò) prev 和 next 指針組成雙端鏈表,如下圖所示:



          為了操作起來(lái)比較方便,Redis 使用了 list 結(jié)構(gòu)持有鏈表。


          typedef struct list {    // 表頭節(jié)點(diǎn)    listNode *head;    // 表尾節(jié)點(diǎn)    listNode *tail;    // 鏈表包含的節(jié)點(diǎn)數(shù)量    unsigned long len;    // 節(jié)點(diǎn)復(fù)制函數(shù)    void *(*dup)(void *ptr);    // 節(jié)點(diǎn)釋放函數(shù)    void (*free)(void *ptr);    // 節(jié)點(diǎn)對(duì)比函數(shù)    int (*match)(void *ptr, void *key);} list;


          list 結(jié)構(gòu)為鏈表提供了表頭指針 head、表尾指針 tail,以及鏈表長(zhǎng)度計(jì)數(shù)器 len,而 dup、free 和 match 成員則是實(shí)現(xiàn)多態(tài)鏈表所需類型的特定函數(shù)。



          Redis 鏈表實(shí)現(xiàn)的特征總結(jié)如下:


          1. 雙端:鏈表節(jié)點(diǎn)帶有 prev 和 next 指針,獲取某個(gè)節(jié)點(diǎn)的前置節(jié)點(diǎn)和后置節(jié)點(diǎn)的復(fù)雜度都是 O(n)

          2. 無(wú)環(huán):表頭節(jié)點(diǎn)的 prev 指針和表尾節(jié)點(diǎn)的 next 指針都指向 NULL,對(duì)鏈表的訪問(wèn)以 NULL 為終點(diǎn);

          3. 帶表頭指針和表尾指針:通過(guò) list 結(jié)構(gòu)的 head 指針和 tail 指針,程序獲取鏈表的表頭節(jié)點(diǎn)和表尾節(jié)點(diǎn)的復(fù)雜度為 O(1)

          4. 帶鏈表長(zhǎng)度計(jì)數(shù)器:程序使用 list 結(jié)構(gòu)的 len 屬性來(lái)對(duì) list 持有的節(jié)點(diǎn)進(jìn)行計(jì)數(shù),程序獲取鏈表中節(jié)點(diǎn)數(shù)量的復(fù)雜度為 O(1)

          5. 多態(tài):鏈表節(jié)點(diǎn)使用 void* 指針來(lái)保存節(jié)點(diǎn)值,可以保存各種不同類型的值。



          3.2 壓縮列表


          壓縮列表(ziplist)是列表鍵和哈希鍵的底層實(shí)現(xiàn)之一。壓縮列表主要目的是為了節(jié)約內(nèi)存,是由一系列特殊編碼的連續(xù)內(nèi)存塊組成的順序型數(shù)據(jù)結(jié)構(gòu)。一個(gè)壓縮列表可以包含任意多個(gè)節(jié)點(diǎn),每個(gè)節(jié)點(diǎn)可以保存一個(gè)字節(jié)數(shù)組或者一個(gè)整數(shù)值。



          如上圖所示,壓縮列表記錄了各組成部分的類型、長(zhǎng)度以及用途。



          4. 哈希對(duì)象


          哈希對(duì)象的編碼可以是 ziplist 或者 hashtable。


          4.1 hash-ziplist


          ziplist 底層使用的是壓縮列表實(shí)現(xiàn),上文已經(jīng)詳細(xì)介紹了壓縮列表的實(shí)現(xiàn)原理。每當(dāng)有新的鍵值對(duì)要加入哈希對(duì)象時(shí),先把保存了鍵的節(jié)點(diǎn)推入壓縮列表表尾,然后再將保存了值的節(jié)點(diǎn)推入壓縮列表表尾。比如,我們執(zhí)行如下三條 HSET 命令:


          HSET profile name "tom"HSET profile age 25HSET profile career "Programmer"


          如果此時(shí)使用 ziplist 編碼,那么該 Hash 對(duì)象在內(nèi)存中的結(jié)構(gòu)如下:



          3.2 hash-hashtable


          hashtable 編碼的哈希對(duì)象使用字典作為底層實(shí)現(xiàn)。字典是一種用于保存鍵值對(duì)的數(shù)據(jù)結(jié)構(gòu),Redis 的字典使用哈希表作為底層實(shí)現(xiàn),一個(gè)哈希表里面可以有多個(gè)哈希表節(jié)點(diǎn),每個(gè)哈希表節(jié)點(diǎn)保存的就是一個(gè)鍵值對(duì)。


          3.3 哈希表


          Redis 使用的哈希表由 dictht 結(jié)構(gòu)定義:


          typedef struct dictht{    // 哈希表數(shù)組    dictEntry **table;
          // 哈希表大小 unsigned long size;
          // 哈希表大小掩碼,用于計(jì)算索引值 // 總是等于 size-1 unsigned long sizemask;
          // 該哈希表已有節(jié)點(diǎn)數(shù)量 unsigned long used;} dictht


          table 屬性是一個(gè)數(shù)組,數(shù)組中的每個(gè)元素都是一個(gè)指向 dictEntry 結(jié)構(gòu)的指針,每個(gè) dictEntry 結(jié)構(gòu)保存著一個(gè)鍵值對(duì)。


          size 屬性記錄了哈希表的大小,即 table 數(shù)組的大小。used 屬性記錄了哈希表目前已有節(jié)點(diǎn)數(shù)量。sizemask 總是等于 size-1,這個(gè)值主要用于數(shù)組索引。


          比如下圖展示了一個(gè)大小為 4 的空哈希表。


          哈希表節(jié)點(diǎn)


          哈希表節(jié)點(diǎn)使用 dictEntry 結(jié)構(gòu)表示,每個(gè) dictEntry 結(jié)構(gòu)都保存著一個(gè)鍵值對(duì):


          typedef struct dictEntry {    // 鍵    void *key;
          // 值 union { void *val; unit64_t u64; nit64_t s64; } v;
          // 指向下一個(gè)哈希表節(jié)點(diǎn),形成鏈表 struct dictEntry *next;} dictEntry;


          key 屬性保存著鍵值對(duì)中的鍵,而 v 屬性則保存了鍵值對(duì)中的值。值可以是一個(gè)指針,一個(gè) uint64_t 整數(shù)或者是 int64_t 整數(shù)。next 屬性指向了另一個(gè) dictEntry 節(jié)點(diǎn),在數(shù)組桶位相同的情況下,將多個(gè) dictEntry 節(jié)點(diǎn)串聯(lián)成一個(gè)鏈表,以此來(lái)解決鍵沖突問(wèn)題(鏈地址法)。


          3.4 字典


          Redis 字典由 dict 結(jié)構(gòu)表示:


          typedef struct dict {    // 類型特定函數(shù)    dictType *type;
          // 私有數(shù)據(jù) void *privdata;
          // 哈希表 dictht ht[2];
          //rehash索引 // 當(dāng)rehash不在進(jìn)行時(shí),值為-1 int rehashidx;}


          ht 是大小為 2,且每個(gè)元素都指向 dictht 哈希表。一般情況下,字典只會(huì)使用 ht[0] 哈希表,ht[1] 哈希表只會(huì)在對(duì) ht[0] 哈希表進(jìn)行 rehash 時(shí)使用。rehashidx 記錄了 rehash 的進(jìn)度,如果目前沒(méi)有進(jìn)行 rehash,值為 -1。


          rehash


          為了使 hash 表的負(fù)載因子 (ht[0]).used/ht[0]).size) 維持在一個(gè)合理范圍,當(dāng)哈希表保存的元素過(guò)多或者過(guò)少時(shí),程序需要對(duì) hash 表進(jìn)行相應(yīng)的擴(kuò)展和收縮。


          rehash(重新散列)操作就是用來(lái)完成 hash 表的擴(kuò)展和收縮的。


          rehash 的步驟如下:


          1. 為 ht [1] 哈希表分配空間;


          • 如果是擴(kuò)展操作,那么 ht[1] 的大小為第一個(gè)大于 ht[0].used*2的2n。比如ht[0].used=5,那么此時(shí) ht[1] 的大小就為 16(大于 10 的第一個(gè) 2n 的值是 16);

          • 如果是收縮操作,那么 ht[1] 的大小為第一個(gè)大于 ht[0].used 的 2n。比如ht[0].used=5,那么此時(shí) ht[1] 的大小就為 8(大于 5 的第一個(gè) 2n 的值是 8)。


          2. 將保存在 ht[0] 中的所有鍵值對(duì) rehash 到 ht[1] 中;


          3. 遷移完成之后,釋放掉 ht[0],并將現(xiàn)在的 ht[1] 設(shè)置為 ht[0],在 ht[1] 新創(chuàng)建一個(gè)空白哈希表,為下一次 rehash 做準(zhǔn)備。


          哈希表的擴(kuò)展和收縮時(shí)機(jī)


          • 當(dāng)服務(wù)器沒(méi)有執(zhí)行 BGSAVE 或者 BGREWRITEAOF 命令時(shí),負(fù)載因子大于等于 1 觸發(fā)哈希表的擴(kuò)展操作;

          • 當(dāng)服務(wù)器在執(zhí)行 BGSAVE 或者 BGREWRITEAOF 命令,負(fù)載因子大于等于 5 觸發(fā)哈希表的擴(kuò)展操作;

          • 當(dāng)哈希表負(fù)載因子小于 0.1,觸發(fā)哈希表的收縮操作。


          漸進(jìn)式 rehash


          前面講過(guò),擴(kuò)展或者收縮需要將 ht[0] 里面的元素全部 rehash 到 ht[1] 中,如果 ht[0] 元素很多,顯然一次性 rehash 成本會(huì)很大,從影響到 Redis 性能。


          為了解決上述問(wèn)題,Redis 使用了漸進(jìn)式 rehash 技術(shù),具體來(lái)說(shuō)就是分多次,漸進(jìn)式地將 ht[0] 里面的元素慢慢地 rehash 到 ht[1] 中。


          下面是漸進(jìn)式 rehash 的詳細(xì)步驟:


          1. 為 ht[1] 分配空間;

          2. 在字典中維持一個(gè)索引計(jì)數(shù)器變量 rehashidx,并將它的值設(shè)置為 0,表示 rehash 正式開(kāi)始;

          3. 在 rehash 進(jìn)行期間,每次對(duì)字典執(zhí)行添加、刪除、查找或者更新時(shí),除了會(huì)執(zhí)行相應(yīng)的操作之外,還會(huì)順帶將 ht[0] 在 rehashidx 索引位上的所有鍵值對(duì) rehash 到 ht[1] 中,rehash 完成之后,rehashidx 值加 1;

          4. 隨著字典操作的不斷進(jìn)行,最終會(huì)在啊某個(gè)時(shí)刻遷移完成,此時(shí)將 rehashidx 值置為 -1,表示 rehash 結(jié)束。


          漸進(jìn)式 rehash 一次遷移一個(gè)桶上所有的數(shù)據(jù)。設(shè)計(jì)上采用分而治之的思想,將原本集中式的操作分散到每個(gè)添加、刪除、查找和更新操作上,從而避免集中式 rehash 帶來(lái)的龐大計(jì)算。


          因?yàn)樵跐u進(jìn)式 rehash 時(shí),字典會(huì)同時(shí)使用 ht[0] 和 ht[1] 兩張表,所以此時(shí)對(duì)字典的刪除、查找和更新操作都可能會(huì)在兩個(gè)哈希表進(jìn)行。比如,如果要查找某個(gè)鍵時(shí),先在 ht[0] 中查找,如果沒(méi)找到,則繼續(xù)到 ht[1] 中查找。


          hash 對(duì)象中的 hashtable


          HSET profile name "tom"HSET profile age 25HSET profile career "Programmer"


          還是上述三條命令,保存數(shù)據(jù)到 Redis 的哈希對(duì)象中,如果采用 hashtable 編碼保存的話,那么該 Hash 對(duì)象在內(nèi)存中的結(jié)構(gòu)如下:



          當(dāng)哈希對(duì)象保存的所有鍵值對(duì)的鍵和值的字符串長(zhǎng)度都小于 64 個(gè)字節(jié),并且數(shù)量小于 512 個(gè)時(shí),使用 ziplist 編碼,否則使用 hashtable 編碼。


          可以通過(guò)配置文件修改該上限值。


          4. 集合對(duì)象


          集合對(duì)象的編碼可以是 intset 或者 hashtable。當(dāng)集合對(duì)象保存的元素都是整數(shù),并且個(gè)數(shù)不超過(guò) 512 個(gè)時(shí),使用 intset 編碼,否則使用 hashtable 編碼。


          4.1 set-intset


          intset 編碼的集合對(duì)象底層使用整數(shù)集合實(shí)現(xiàn)。


          整數(shù)集合(intset)是 Redis 用于保存整數(shù)值的集合抽象數(shù)據(jù)結(jié)構(gòu),它可以保存類型為 int16_t、int32_t 或者 int64_t 的整數(shù)值,并且保證集合中的數(shù)據(jù)不會(huì)重復(fù)。Redis 使用 intset 結(jié)構(gòu)表示一個(gè)整數(shù)集合。


          typedef struct intset {    // 編碼方式    uint32_t encoding;    // 集合包含的元素?cái)?shù)量    uint32_t length;    // 保存元素的數(shù)組    int8_t contents[];} intset;


          contents 數(shù)組是整數(shù)集合的底層實(shí)現(xiàn):整數(shù)集合的每個(gè)元素都是 contents 數(shù)組的一個(gè)數(shù)組項(xiàng),各個(gè)項(xiàng)在數(shù)組中按值大小從小到大有序排列,并且數(shù)組中不包含重復(fù)項(xiàng)。


          雖然 contents 屬性聲明為 int8_t 類型的數(shù)組,但實(shí)際上,contents 數(shù)組不保存任何 int8_t 類型的值,數(shù)組中真正保存的值類型取決于 encoding。


          如果 encoding 屬性值為 INTSET_ENC_INT16,那么 contents 數(shù)組就是 int16_t 類型的數(shù)組,以此類推。


          當(dāng)新插入元素的類型比整數(shù)集合現(xiàn)有類型元素的類型大時(shí),整數(shù)集合必須先升級(jí),然后才能將新元素添加進(jìn)來(lái)。這個(gè)過(guò)程分以下三步進(jìn)行:


          1. 根據(jù)新元素類型,擴(kuò)展整數(shù)集合底層數(shù)組空間大小;

          2. 將底層數(shù)組現(xiàn)有所有元素都轉(zhuǎn)換為與新元素相同的類型,并且維持底層數(shù)組的有序性;

          3. 將新元素添加到底層數(shù)組里面。


          還有一點(diǎn)需要注意的是,整數(shù)集合不支持降級(jí)。一旦對(duì)數(shù)組進(jìn)行了升級(jí),編碼就會(huì)一直保持升級(jí)后的狀態(tài)。


          舉個(gè)例子,當(dāng)執(zhí)行 SADD numbers 1 3 5 向集合對(duì)象插入數(shù)據(jù)時(shí),該集合對(duì)象在內(nèi)存的結(jié)構(gòu)如下:


          4.2 set-hashtable


          hashtable 編碼的集合對(duì)象使用字典作為底層實(shí)現(xiàn)。字典的每個(gè)鍵都是一個(gè)字符串對(duì)象,每個(gè)字符串對(duì)象對(duì)應(yīng)一個(gè)集合元素,字典的值都是 NULL。


          當(dāng)我們執(zhí)行 SADD fruits "apple" "banana" "cherry" 向集合對(duì)象插入數(shù)據(jù)時(shí),該集合對(duì)象在內(nèi)存的結(jié)構(gòu)如下:



          5. 有序集合對(duì)象


          有序集合的編碼可以是 ziplist 或者 skiplist。當(dāng)有序集合保存的元素個(gè)數(shù)小于 128 個(gè),且所有元素成員長(zhǎng)度都小于 64 字節(jié)時(shí),使用 ziplist 編碼,否則使用 skiplist 編碼。


          5.1 zset-ziplist


          ziplist 編碼的有序集合使用壓縮列表作為底層實(shí)現(xiàn)。每個(gè)集合元素使用兩個(gè)緊挨著一起的兩個(gè)壓縮列表節(jié)點(diǎn)表示,第一個(gè)節(jié)點(diǎn)保存元素的成員(member),第二個(gè)節(jié)點(diǎn)保存元素的分值(score)。


          壓縮列表內(nèi)的集合元素按照分值從小到大排列。如果我們執(zhí)行 ZADD price 8.5 apple 5.0 banana 6.0 cherry 命令向有序集合插入元素,該有序集合在內(nèi)存中的結(jié)構(gòu)如下:



          5.2 zset-skiplist


          skiplist 編碼的有序集合對(duì)象使用 zset 結(jié)構(gòu)作為底層實(shí)現(xiàn),一個(gè) zset 結(jié)構(gòu)同時(shí)包含一個(gè)字典和一個(gè)跳躍表。


          typedef struct zset {    zskiplist *zs1;    dict *dict;}


          繼續(xù)介紹之前,我們先了解一下什么是跳躍表。


          跳躍表


          跳躍表(skiplist)是一種有序的數(shù)據(jù)結(jié)構(gòu),它通過(guò)在每個(gè)節(jié)點(diǎn)中維持多個(gè)指向其他節(jié)點(diǎn)的指針,從而達(dá)到快速訪問(wèn)節(jié)點(diǎn)的目的。


          Redis 的跳躍表由 zskiplistNode 和 zskiplist 兩個(gè)結(jié)構(gòu)定義。zskiplistNode 結(jié)構(gòu)表示跳躍表節(jié)點(diǎn),zskiplist 保存跳躍表節(jié)點(diǎn)相關(guān)信息,比如節(jié)點(diǎn)的數(shù)量,以及指向表頭和表尾節(jié)點(diǎn)的指針等。


          跳躍表節(jié)點(diǎn) zskiplistNode


          跳躍表節(jié)點(diǎn) zskiplistNode 結(jié)構(gòu)定義如下:


          typedef struct zskiplistNode {    // 后退指針    struct zskiplistNode *backward;    // 分值    double score;    // 成員對(duì)象    robj *obj;    // 層    struct zskiplistLevel {        // 前進(jìn)指針        struct zskiplistNode *forward;        // 跨度        unsigned int span;    } level[];} zskiplistNode;


          下圖是一個(gè)層高為 5,包含 4 個(gè)跳躍表節(jié)點(diǎn)(1 個(gè)表頭節(jié)點(diǎn)和 3 個(gè)數(shù)據(jù)節(jié)點(diǎn))組成的跳躍表:



          有序集合對(duì)象的 skiplist 實(shí)現(xiàn)


          前面講過(guò),skiplist 編碼的有序集合對(duì)象使用 zset 結(jié)構(gòu)作為底層實(shí)現(xiàn)。一個(gè) zset 結(jié)構(gòu)同時(shí)包含一個(gè)字典和一個(gè)跳躍表。


          typedef struct zset {    zskiplist *zs1;    dict *dict;}


          zset 結(jié)構(gòu)中的 zs1 跳躍表按分值從小到大保存了所有集合元素,每個(gè)跳躍表節(jié)點(diǎn)都保存了一個(gè)集合元素。


          通過(guò)跳躍表,可以對(duì)有序集合進(jìn)行基于 score 的快速范圍查找。zset 結(jié)構(gòu)中的 dict 字典為有序集合創(chuàng)建了從成員到分值的映射,字典的鍵保存了成員,字典的值保存了分值。通過(guò)字典,可以用 O(1) 復(fù)雜度查找給定成員的分值。


          假如還是執(zhí)行 ZADD price 8.5 apple 5.0 banana 6.0 cherry 命令向 zset 保存數(shù)據(jù),如果采用 skiplist 編碼方式的話,該有序集合在內(nèi)存中的結(jié)構(gòu)如下:


          6. 總結(jié)


          總的來(lái)說(shuō),Redis 底層數(shù)據(jù)結(jié)構(gòu)主要包括簡(jiǎn)單動(dòng)態(tài)字符串(SDS)、鏈表、字典、跳躍表、整數(shù)集合和壓縮列表六種類型。并且基于這些基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)了字符串對(duì)象、列表對(duì)象、哈希對(duì)象、集合對(duì)象以及有序集合對(duì)象五種常見(jiàn)的對(duì)象類型。每一種對(duì)象類型都至少采用了 2 種數(shù)據(jù)編碼,不同的編碼使用的底層數(shù)據(jù)結(jié)構(gòu)也不同。

          商城源碼下載



          商城管理后端演示圖



          源碼地址獲取方法,老規(guī)矩啦!

          識(shí)別下方二維碼,關(guān)注后回復(fù)【A107】

          即可獲取下載鏈接

          瀏覽 29
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  天天爽夜夜爽AA片免费 | 壁特壁视频免费观看 | 天天天天天操 | 国内精自视频在线观看 | 影音先锋每日资源啪啪AV |