日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 运维知识 > 数据库 >内容正文

数据库

Redis 中的集合类型是怎么实现的?

發(fā)布時(shí)間:2025/4/16 数据库 32 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Redis 中的集合类型是怎么实现的? 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

本文是《Redis內(nèi)部數(shù)據(jù)結(jié)構(gòu)詳解》系列的第七篇。在本文中,我們圍繞一個(gè)Redis的內(nèi)部數(shù)據(jù)結(jié)構(gòu)——intset展開(kāi)討論。

Redis里面使用intset是為了實(shí)現(xiàn)集合(set)這種對(duì)外的數(shù)據(jù)結(jié)構(gòu)。set結(jié)構(gòu)類似于數(shù)學(xué)上的集合的概念,它包含的元素?zé)o序,且不能重復(fù)。Redis里的set結(jié)構(gòu)還實(shí)現(xiàn)了基礎(chǔ)的集合并、交、差的操作。與Redis對(duì)外暴露的其它數(shù)據(jù)結(jié)構(gòu)類似,set的底層實(shí)現(xiàn),隨著元素類型是否是整型以及添加的元素的數(shù)目多少,而有所變化。概括來(lái)講,當(dāng)set中添加的元素都是整型且元素?cái)?shù)目較少時(shí),set使用intset作為底層數(shù)據(jù)結(jié)構(gòu),否則,set使用dict作為底層數(shù)據(jù)結(jié)構(gòu)。

在本文中我們將大體分成三個(gè)部分進(jìn)行介紹:

  • 集中介紹intset數(shù)據(jù)結(jié)構(gòu)。
  • 討論set是如何在intset和dict基礎(chǔ)上構(gòu)建起來(lái)的。
  • 集中討論set的并、交、差的算法實(shí)現(xiàn)以及時(shí)間復(fù)雜度。注意,其中差集的計(jì)算在Redis中實(shí)現(xiàn)了兩種算法。
  • 我們?cè)谟懻撝羞€會(huì)涉及到一個(gè)Redis配置(在redis.conf中的ADVANCED CONFIG部分):

    set-max-intset-entries 512復(fù)制代碼

    注:本文討論的代碼實(shí)現(xiàn)基于Redis源碼的3.2分支。

    intset數(shù)據(jù)結(jié)構(gòu)簡(jiǎn)介

    intset顧名思義,是由整數(shù)組成的集合。實(shí)際上,intset是一個(gè)由整數(shù)組成的有序集合,從而便于在上面進(jìn)行二分查找,用于快速地判斷一個(gè)元素是否屬于這個(gè)集合。它在內(nèi)存分配上與ziplist有些類似,是連續(xù)的一整塊內(nèi)存空間,而且對(duì)于大整數(shù)和小整數(shù)(按絕對(duì)值)采取了不同的編碼,盡量對(duì)內(nèi)存的使用進(jìn)行了優(yōu)化。

    intset的數(shù)據(jù)結(jié)構(gòu)定義如下(出自intset.h和intset.c):

    typedef struct intset {uint32_t encoding;uint32_t length;int8_t contents[]; } intset;#define INTSET_ENC_INT16 (sizeof(int16_t)) #define INTSET_ENC_INT32 (sizeof(int32_t)) #define INTSET_ENC_INT64 (sizeof(int64_t))復(fù)制代碼

    各個(gè)字段含義如下:

    • encoding: 數(shù)據(jù)編碼,表示intset中的每個(gè)數(shù)據(jù)元素用幾個(gè)字節(jié)來(lái)存儲(chǔ)。它有三種可能的取值:INTSET_ENC_INT16表示每個(gè)元素用2個(gè)字節(jié)存儲(chǔ),INTSET_ENC_INT32表示每個(gè)元素用4個(gè)字節(jié)存儲(chǔ),INTSET_ENC_INT64表示每個(gè)元素用8個(gè)字節(jié)存儲(chǔ)。因此,intset中存儲(chǔ)的整數(shù)最多只能占用64bit。
    • length: 表示intset中的元素個(gè)數(shù)。encoding和length兩個(gè)字段構(gòu)成了intset的頭部(header)。
    • contents: 是一個(gè)柔性數(shù)組(flexible array member),表示intset的header后面緊跟著數(shù)據(jù)元素。這個(gè)數(shù)組的總長(zhǎng)度(即總字節(jié)數(shù))等于encoding * length。柔性數(shù)組在Redis的很多數(shù)據(jù)結(jié)構(gòu)的定義中都出現(xiàn)過(guò)(例如sds, quicklist, skiplist),用于表達(dá)一個(gè)偏移量。contents需要單獨(dú)為其分配空間,這部分內(nèi)存不包含在intset結(jié)構(gòu)當(dāng)中。

    其中需要注意的是,intset可能會(huì)隨著數(shù)據(jù)的添加而改變它的數(shù)據(jù)編碼:

    • 最開(kāi)始,新創(chuàng)建的intset使用占內(nèi)存最小的INTSET_ENC_INT16(值為2)作為數(shù)據(jù)編碼。
    • 每添加一個(gè)新元素,則根據(jù)元素大小決定是否對(duì)數(shù)據(jù)編碼進(jìn)行升級(jí)。

    下圖給出了一個(gè)添加數(shù)據(jù)的具體例子(點(diǎn)擊看大圖)。

    在上圖中:

    • 新創(chuàng)建的intset只有一個(gè)header,總共8個(gè)字節(jié)。其中encoding = 2, length = 0。
    • 添加13, 5兩個(gè)元素之后,因?yàn)樗鼈兪潜容^小的整數(shù),都能使用2個(gè)字節(jié)表示,所以encoding不變,值還是2。
    • 當(dāng)添加32768的時(shí)候,它不再能用2個(gè)字節(jié)來(lái)表示了(2個(gè)字節(jié)能表達(dá)的數(shù)據(jù)范圍是-215~215-1,而32768等于215,超出范圍了),因此encoding必須升級(jí)到INTSET_ENC_INT32(值為4),即用4個(gè)字節(jié)表示一個(gè)元素。
    • 在添加每個(gè)元素的過(guò)程中,intset始終保持從小到大有序。
    • 與ziplist類似,intset也是按小端(little endian)模式存儲(chǔ)的(參見(jiàn)維基百科詞條Endianness)。比如,在上圖中intset添加完所有數(shù)據(jù)之后,表示encoding字段的4個(gè)字節(jié)應(yīng)該解釋成0x00000004,而第5個(gè)數(shù)據(jù)應(yīng)該解釋成0x000186A0 = 100000。

    intset與ziplist相比:

    • ziplist可以存儲(chǔ)任意二進(jìn)制串,而intset只能存儲(chǔ)整數(shù)。
    • ziplist是無(wú)序的,而intset是從小到大有序的。因此,在ziplist上查找只能遍歷,而在intset上可以進(jìn)行二分查找,性能更高。
    • ziplist可以對(duì)每個(gè)數(shù)據(jù)項(xiàng)進(jìn)行不同的變長(zhǎng)編碼(每個(gè)數(shù)據(jù)項(xiàng)前面都有數(shù)據(jù)長(zhǎng)度字段len),而intset只能整體使用一個(gè)統(tǒng)一的編碼(encoding)。

    intset的查找和添加操作

    要理解intset的一些實(shí)現(xiàn)細(xì)節(jié),只需要關(guān)注intset的兩個(gè)關(guān)鍵操作基本就可以了:查找(intsetFind)和添加(intsetAdd)元素。

    intsetFind的關(guān)鍵代碼如下所示(出自intset.c):

    uint8_t intsetFind(intset *is, int64_t value) {uint8_t valenc = _intsetValueEncoding(value);return valenc <= intrev32ifbe(is->encoding) && intsetSearch(is,value,NULL); }static uint8_t intsetSearch(intset *is, int64_t value, uint32_t *pos) {int min = 0, max = intrev32ifbe(is->length)-1, mid = -1;int64_t cur = -1;/* The value can never be found when the set is empty */if (intrev32ifbe(is->length) == 0) {if (pos) *pos = 0;return 0;} else {/* Check for the case where we know we cannot find the value,* but do know the insert position. */if (value > _intsetGet(is,intrev32ifbe(is->length)-1)) {if (pos) *pos = intrev32ifbe(is->length);return 0;} else if (value < _intsetGet(is,0)) {if (pos) *pos = 0;return 0;}}while(max >= min) {mid = ((unsigned int)min + (unsigned int)max) >> 1;cur = _intsetGet(is,mid);if (value > cur) {min = mid+1;} else if (value < cur) {max = mid-1;} else {break;}}if (value == cur) {if (pos) *pos = mid;return 1;} else {if (pos) *pos = min;return 0;} }復(fù)制代碼

    關(guān)于以上代碼,我們需要注意的地方包括:

    • intsetFind在指定的intset中查找指定的元素value,找到返回1,沒(méi)找到返回0。
    • _intsetValueEncoding函數(shù)會(huì)根據(jù)要查找的value落在哪個(gè)范圍而計(jì)算出相應(yīng)的數(shù)據(jù)編碼(即它應(yīng)該用幾個(gè)字節(jié)來(lái)存儲(chǔ))。
    • 如果value所需的數(shù)據(jù)編碼比當(dāng)前intset的編碼要大,則它肯定在當(dāng)前intset所能存儲(chǔ)的數(shù)據(jù)范圍之外(特別大或特別小),所以這時(shí)會(huì)直接返回0;否則調(diào)用intsetSearch執(zhí)行一個(gè)二分查找算法。
    • intsetSearch在指定的intset中查找指定的元素value,如果找到,則返回1并且將參數(shù)pos指向找到的元素位置;如果沒(méi)找到,則返回0并且將參數(shù)pos指向能插入該元素的位置。
    • intsetSearch是對(duì)于二分查找算法的一個(gè)實(shí)現(xiàn),它大致分為三個(gè)部分:
      • 特殊處理intset為空的情況。
      • 特殊處理兩個(gè)邊界情況:當(dāng)要查找的value比最后一個(gè)元素還要大或者比第一個(gè)元素還要小的時(shí)候。實(shí)際上,這兩部分的特殊處理,在二分查找中并不是必須的,但它們?cè)谶@里提供了特殊情況下快速失敗的可能。
      • 真正執(zhí)行二分查找過(guò)程。注意:如果最后沒(méi)找到,插入位置在min指定的位置。
    • 代碼中出現(xiàn)的intrev32ifbe是為了在需要的時(shí)候做大小端轉(zhuǎn)換的。前面我們提到過(guò),intset里的數(shù)據(jù)是按小端(little endian)模式存儲(chǔ)的,因此在大端(big endian)機(jī)器上運(yùn)行時(shí),這里的intrev32ifbe會(huì)做相應(yīng)的轉(zhuǎn)換。
    • 這個(gè)查找算法的總的時(shí)間復(fù)雜度為O(log n)。

    而intsetAdd的關(guān)鍵代碼如下所示(出自intset.c):

    intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {uint8_t valenc = _intsetValueEncoding(value);uint32_t pos;if (success) *success = 1;/* Upgrade encoding if necessary. If we need to upgrade, we know that* this value should be either appended (if > 0) or prepended (if < 0),* because it lies outside the range of existing values. */if (valenc > intrev32ifbe(is->encoding)) {/* This always succeeds, so we don't need to curry *success. */return intsetUpgradeAndAdd(is,value);} else {/* Abort if the value is already present in the set.* This call will populate "pos" with the right position to insert* the value when it cannot be found. */if (intsetSearch(is,value,&pos)) {if (success) *success = 0;return is;}is = intsetResize(is,intrev32ifbe(is->length)+1);if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);}_intsetSet(is,pos,value);is->length = intrev32ifbe(intrev32ifbe(is->length)+1);return is; }復(fù)制代碼

    關(guān)于以上代碼,我們需要注意的地方包括:

    • intsetAdd在intset中添加新元素value。如果value在添加前已經(jīng)存在,則不會(huì)重復(fù)添加,這時(shí)參數(shù)success被置為0;如果value在原來(lái)intset中不存在,則將value插入到適當(dāng)位置,這時(shí)參數(shù)success被置為0。
    • 如果要添加的元素value所需的數(shù)據(jù)編碼比當(dāng)前intset的編碼要大,那么則調(diào)用intsetUpgradeAndAdd將intset的編碼進(jìn)行升級(jí)后再插入value。
    • 調(diào)用intsetSearch,如果能查到,則不會(huì)重復(fù)添加。
    • 如果沒(méi)查到,則調(diào)用intsetResize對(duì)intset進(jìn)行內(nèi)存擴(kuò)充,使得它能夠容納新添加的元素。因?yàn)閕ntset是一塊連續(xù)空間,因此這個(gè)操作會(huì)引發(fā)內(nèi)存的realloc(參見(jiàn)man.cx/realloc)。這有可能帶來(lái)一次數(shù)據(jù)拷貝。同時(shí)調(diào)用intsetMoveTail將待插入位置后面的元素統(tǒng)一向后移動(dòng)1個(gè)位置,這也涉及到一次數(shù)據(jù)拷貝。值得注意的是,在intsetMoveTail中是調(diào)用memmove完成這次數(shù)據(jù)拷貝的。memmove保證了在拷貝過(guò)程中不會(huì)造成數(shù)據(jù)重疊或覆蓋,具體參見(jiàn)man.cx/memmove。
    • intsetUpgradeAndAdd的實(shí)現(xiàn)中也會(huì)調(diào)用intsetResize來(lái)完成內(nèi)存擴(kuò)充。在進(jìn)行編碼升級(jí)時(shí),intsetUpgradeAndAdd的實(shí)現(xiàn)會(huì)把原來(lái)intset中的每個(gè)元素取出來(lái),再用新的編碼重新寫入新的位置。
    • 注意一下intsetAdd的返回值,它返回一個(gè)新的intset指針。它可能與傳入的intset指針is相同,也可能不同。調(diào)用方必須用這里返回的新的intset,替換之前傳進(jìn)來(lái)的舊的intset變量。類似這種接口使用模式,在Redis的實(shí)現(xiàn)代碼中是很常見(jiàn)的,比如我們之前在介紹sds和ziplist的時(shí)候都碰到過(guò)類似的情況。
    • 顯然,這個(gè)intsetAdd算法總的時(shí)間復(fù)雜度為O(n)。

    Redis的set

    為了更好地理解Redis對(duì)外暴露的set數(shù)據(jù)結(jié)構(gòu),我們先看一下set的一些關(guān)鍵的命令。下面是一些命令舉例:

    上面這些命令的含義:

    • sadd用于分別向集合s1和s2中添加元素。添加的元素既有數(shù)字,也有非數(shù)字("a"和"b")。
    • sismember用于判斷指定的元素是否在集合內(nèi)存在。
    • sinter, sunion和sdiff分別用于計(jì)算集合的交集、并集和差集。

    我們前面提到過(guò),set的底層實(shí)現(xiàn),隨著元素類型是否是整型以及添加的元素的數(shù)目多少,而有所變化。例如,具體到上述命令的執(zhí)行過(guò)程中,集合s1的底層數(shù)據(jù)結(jié)構(gòu)會(huì)發(fā)生如下變化:

    • 在開(kāi)始執(zhí)行完sadd s1 13 5之后,由于添加的都是比較小的整數(shù),所以s1底層是一個(gè)intset,其數(shù)據(jù)編碼encoding = 2。
    • 在執(zhí)行完sadd s1 32768 10 100000之后,s1底層仍然是一個(gè)intset,但其數(shù)據(jù)編碼encoding從2升級(jí)到了4。
    • 在執(zhí)行完sadd s1 a b之后,由于添加的元素不再是數(shù)字,s1底層的實(shí)現(xiàn)會(huì)轉(zhuǎn)成一個(gè)dict。

    我們知道,dict是一個(gè)用于維護(hù)key和value映射關(guān)系的數(shù)據(jù)結(jié)構(gòu),那么當(dāng)set底層用dict表示的時(shí)候,它的key和value分別是什么呢?實(shí)際上,key就是要添加的集合元素,而value是NULL。

    除了前面提到的由于添加非數(shù)字元素造成集合底層由intset轉(zhuǎn)成dict之外,還有兩種情況可能造成這種轉(zhuǎn)換:

    • 添加了一個(gè)數(shù)字,但它無(wú)法用64bit的有符號(hào)數(shù)來(lái)表達(dá)。intset能夠表達(dá)的最大的整數(shù)范圍為-264~264-1,因此,如果添加的數(shù)字超出了這個(gè)范圍,這也會(huì)導(dǎo)致intset轉(zhuǎn)成dict。
    • 添加的集合元素個(gè)數(shù)超過(guò)了set-max-intset-entries配置的值的時(shí)候,也會(huì)導(dǎo)致intset轉(zhuǎn)成dict(具體的觸發(fā)條件參見(jiàn)t_set.c中的setTypeAdd相關(guān)代碼)。

    對(duì)于小集合使用intset來(lái)存儲(chǔ),主要的原因是節(jié)省內(nèi)存。特別是當(dāng)存儲(chǔ)的元素個(gè)數(shù)較少的時(shí)候,dict所帶來(lái)的內(nèi)存開(kāi)銷要大得多(包含兩個(gè)哈希表、鏈表指針以及大量的其它元數(shù)據(jù))。所以,當(dāng)存儲(chǔ)大量的小集合而且集合元素都是數(shù)字的時(shí)候,用intset能節(jié)省下一筆可觀的內(nèi)存空間。

    實(shí)際上,從時(shí)間復(fù)雜度上比較,intset的平均情況是沒(méi)有dict性能高的。以查找為例,intset是O(log n)的,而dict可以認(rèn)為是O(1)的。但是,由于使用intset的時(shí)候集合元素個(gè)數(shù)比較少,所以這個(gè)影響不大。

    Redis set的并、交、差算法

    Redis set的并、交、差算法的實(shí)現(xiàn)代碼,在t_set.c中。其中計(jì)算交集調(diào)用的是sinterGenericCommand,計(jì)算并集和差集調(diào)用的是sunionDiffGenericCommand。它們都能同時(shí)對(duì)多個(gè)(可以多于2個(gè))集合進(jìn)行運(yùn)算。當(dāng)對(duì)多個(gè)集合進(jìn)行差集運(yùn)算時(shí),它表達(dá)的含義是:用第一個(gè)集合與第二個(gè)集合做差集,所得結(jié)果再與第三個(gè)集合做差集,依次向后類推。

    我們?cè)谶@里簡(jiǎn)要介紹一下三個(gè)算法的實(shí)現(xiàn)思路。

    交集

    計(jì)算交集的過(guò)程大概可以分為三部分:

  • 檢查各個(gè)集合,對(duì)于不存在的集合當(dāng)做空集來(lái)處理。一旦出現(xiàn)空集,則不用繼續(xù)計(jì)算了,最終的交集就是空集。
  • 對(duì)各個(gè)集合按照元素個(gè)數(shù)由少到多進(jìn)行排序。這個(gè)排序有利于后面計(jì)算的時(shí)候從最小的集合開(kāi)始,需要處理的元素個(gè)數(shù)較少。
  • 對(duì)排序后第一個(gè)集合(也就是最小集合)進(jìn)行遍歷,對(duì)于它的每一個(gè)元素,依次在后面的所有集合中進(jìn)行查找。只有在所有集合中都能找到的元素,才加入到最后的結(jié)果集合中。
  • 需要注意的是,上述第3步在集合中進(jìn)行查找,對(duì)于intset和dict的存儲(chǔ)來(lái)說(shuō)時(shí)間復(fù)雜度分別是O(log n)和O(1)。但由于只有小集合才使用intset,所以可以粗略地認(rèn)為intset的查找也是常數(shù)時(shí)間復(fù)雜度的。因此,如Redis官方文檔上所說(shuō)(redis.io/commands/si…),sinter命令的時(shí)間復(fù)雜度為:

    O(N*M) worst case where N is the cardinality of the smallest set and M is the number of sets.

    并集

    計(jì)算并集最簡(jiǎn)單,只需要遍歷所有集合,將每一個(gè)元素都添加到最后的結(jié)果集合中。向集合中添加元素會(huì)自動(dòng)去重。

    由于要遍歷所有集合的每個(gè)元素,所以Redis官方文檔給出的sunion命令的時(shí)間復(fù)雜度為(redis.io/commands/su…):

    O(N) where N is the total number of elements in all given sets.

    注意,這里同前面討論交集計(jì)算一樣,將元素插入到結(jié)果集合的過(guò)程,忽略intset的情況,認(rèn)為時(shí)間復(fù)雜度為O(1)。

    差集

    計(jì)算差集有兩種可能的算法,它們的時(shí)間復(fù)雜度有所區(qū)別。

    第一種算法:

    • 對(duì)第一個(gè)集合進(jìn)行遍歷,對(duì)于它的每一個(gè)元素,依次在后面的所有集合中進(jìn)行查找。只有在所有集合中都找不到的元素,才加入到最后的結(jié)果集合中。

    這種算法的時(shí)間復(fù)雜度為O(N*M),其中N是第一個(gè)集合的元素個(gè)數(shù),M是集合數(shù)目。

    第二種算法:

    • 將第一個(gè)集合的所有元素都加入到一個(gè)中間集合中。
    • 遍歷后面所有的集合,對(duì)于碰到的每一個(gè)元素,從中間集合中刪掉它。
    • 最后中間集合剩下的元素就構(gòu)成了差集。

    這種算法的時(shí)間復(fù)雜度為O(N),其中N是所有集合的元素個(gè)數(shù)總和。

    在計(jì)算差集的開(kāi)始部分,會(huì)先分別估算一下兩種算法預(yù)期的時(shí)間復(fù)雜度,然后選擇復(fù)雜度低的算法來(lái)進(jìn)行運(yùn)算。還有兩點(diǎn)需要注意:

    • 在一定程度上優(yōu)先選擇第一種算法,因?yàn)樗婕暗降牟僮鞅容^少,只用添加,而第二種算法要先添加再刪除。
    • 如果選擇了第一種算法,那么在執(zhí)行該算法之前,Redis的實(shí)現(xiàn)中對(duì)于第二個(gè)集合之后的所有集合,按照元素個(gè)數(shù)由多到少進(jìn)行了排序。這個(gè)排序有利于以更大的概率查找到元素,從而更快地結(jié)束查找。

    對(duì)于sdiff的時(shí)間復(fù)雜度,Redis官方文檔(redis.io/commands/sd…)只給出了第二種算法的結(jié)果,是不準(zhǔn)確的。


    系列下一篇待續(xù),敬請(qǐng)期待。

    (完)

    其它精選文章

    • Redis為什么用跳表而不用平衡樹(shù)?
    • Redis內(nèi)部數(shù)據(jù)結(jié)構(gòu)詳解(5)——quicklist
    • Redis內(nèi)部數(shù)據(jù)結(jié)構(gòu)詳解(4)——ziplist
    • Redis內(nèi)部數(shù)據(jù)結(jié)構(gòu)詳解(3)——robj
    • Redis內(nèi)部數(shù)據(jù)結(jié)構(gòu)詳解(2)——sds
    • Redis內(nèi)部數(shù)據(jù)結(jié)構(gòu)詳解(1)——dict
    • 小白的數(shù)據(jù)進(jìn)階之路
    • 互聯(lián)網(wǎng)風(fēng)雨十年,我所經(jīng)歷的技術(shù)變遷
    • 你需要了解深度學(xué)習(xí)和神經(jīng)網(wǎng)絡(luò)這項(xiàng)技術(shù)嗎?
    • 論人生之轉(zhuǎn)折
    • 技術(shù)的正宗與野路子

    總結(jié)

    以上是生活随笔為你收集整理的Redis 中的集合类型是怎么实现的?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

    如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。