Redis的五種數(shù)據(jù)結(jié)構(gòu)及其底層實(shí)現(xiàn)原理

五種數(shù)據(jù)類型

  • string字符串
  • hash 哈希
  • list 列表
  • set 無序集合
  • zset 有序集合

string字符串類型實(shí)現(xiàn)

redis的字符串類型是由一種叫做簡單動(dòng)態(tài)字符串(SDS)的數(shù)據(jù)類型來實(shí)現(xiàn)

struct sdshdr {
  int len;          //buf中已占用空間的長度
  int free;        //buf中剩余空間的長度
  char buf[];    //數(shù)據(jù)空間
}

SDC和C語言字符串的區(qū)別:
1:SDS保存了字符串的長度烹卒,而C語言不保存,只能遍歷找到第一個(gè)\0的結(jié)束符才能確定字符串的長度
2:修改SDS贸宏,會(huì)檢查空間是否足夠丈探,不足會(huì)先擴(kuò)展空間妇拯,防止緩沖區(qū)溢出蛛芥,C字符串不會(huì)檢查
3:SDS的預(yù)分配空間機(jī)制商膊,可以減少為字符串重新分配空間的次數(shù)
備注:重新分配空間方式饶囚,小于1M的數(shù)據(jù) 翻倍+1帕翻,例如:13K+13K+1,如果大于1M萝风,每次多分配1M嘀掸,例如:10M+1M+1,如果字符串變短,并不會(huì)立即縮短规惰,而是采用惰性空間釋放睬塌,有專門的API可以釋放多余空間

hash哈希

hash結(jié)構(gòu)里其實(shí)是一個(gè)字典,有許多的鍵值對(duì)
redis的哈希表是一個(gè)dictht結(jié)構(gòu)體:

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

哈希表節(jié)點(diǎn)的結(jié)構(gòu)體如下:

typedef struct dictEntry{
  void *key;  //鍵
  union {      //不同鍵對(duì)飲的值得類型可能不同揩晴,使用union來處理這個(gè)問題
    void *val;
    uint64_tu64;
    int64_ts64;
  }
  struct dictEntry *next;
}

hash算法:
當(dāng)要將一個(gè)新的鍵值對(duì)添加到字典里面時(shí), 程序需要先根據(jù)鍵值對(duì)的鍵計(jì)算出哈希值和索引值贪磺, 然后再根據(jù)索引值硫兰, 將包含新鍵值對(duì)的哈希表節(jié)點(diǎn)放到哈希表數(shù)組的指定索引上面。

# 使用字典設(shè)置的哈希函數(shù)寒锚,計(jì)算鍵 key 的哈希值
hash = dict->type->hashFunction(key);
# 使用哈希表的 sizemask 屬性和哈希值劫映,計(jì)算出索引值
# 根據(jù)情況不同违孝, ht[x] 可以是 ht[0] 或者 ht[1]
index = hash & dict->ht[x].sizemask;

hash沖突解決方式:鏈表法,后入的放到最前面
rehash:
鍵值數(shù)據(jù)量變動(dòng)時(shí)泳赋,時(shí)為了讓哈希表的負(fù)載因子(load factor)維持在一個(gè)合理的范圍之內(nèi)雌桑, 當(dāng)哈希表保存的鍵值對(duì)數(shù)量太多或者太少時(shí), 程序需要對(duì)哈希表的大小進(jìn)行相應(yīng)的擴(kuò)展或者收縮祖今。
如果是擴(kuò)充筹燕,新數(shù)組的空間大小為 大于2*used的2的n次方,比如:used=5,則去大于10的第一個(gè)2的n次方衅鹿,為16
如果是縮小撒踪,新數(shù)組的空間大小為第一個(gè)不大于used的2的n次方,比如:used=5,則新大小為4

list列表類型實(shí)現(xiàn)

redis的list列表是使用雙向鏈表來實(shí)現(xiàn)的
···
typedef struct listNode {
struct listNode * pre; //前置節(jié)點(diǎn)
struct listNode * next; //后置節(jié)點(diǎn)
void * value; //節(jié)點(diǎn)的值
}

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

zset有序集合

1:有序集合的底層實(shí)現(xiàn)之一是跳表大渤, 除此之外跳表它在 Redis 中沒有其他應(yīng)用制妄。
2:整數(shù)集合(intset)是集合鍵的底層實(shí)現(xiàn)之一: 當(dāng)一個(gè)集合只包含整數(shù)值元素, 并且這個(gè)集合的元素?cái)?shù)量不多時(shí)泵三, Redis 就會(huì)使用整數(shù)集合作為集合鍵的底層實(shí)現(xiàn)耕捞。
3:數(shù)據(jù)少是,使用ziplist(壓縮列表)烫幕,占用連續(xù)內(nèi)存俺抽,每項(xiàng)元素都是(數(shù)據(jù)+score)的方式連續(xù)存儲(chǔ),按照score從小到大排序较曼。ziplist為了節(jié)省內(nèi)存磷斧,每個(gè)元素占用的空間可以不同,對(duì)于大數(shù)據(jù)(long long),就多用一些字節(jié)存儲(chǔ)捷犹,而對(duì)于小的數(shù)據(jù)(short)弛饭,就少用一些字節(jié)來存儲(chǔ)。因此查找的時(shí)候需要按順序遍歷萍歉。ziplist省內(nèi)存但是查找效率低侣颂。

set無序集合

無序集合可以用整數(shù)集合(intset)或者字典實(shí)現(xiàn)

Stream

Redis的5.0版本中,放出一個(gè)新的數(shù)據(jù)結(jié)構(gòu)Stream枪孩。其實(shí)也是一個(gè)隊(duì)列憔晒,沒一個(gè)不同的key對(duì)應(yīng)的是不同的隊(duì)列,沒個(gè)隊(duì)列的元素蔑舞,也就是消息拒担,都有一個(gè)msgid,并且需要保證msgid是嚴(yán)格遞增的斗幼。在Stream當(dāng)中澎蛛,消息是默認(rèn)持久化的,即便是Redis重啟蜕窿,也能夠讀取到信息谋逻。
Stream的多播呆馁,與其它隊(duì)列系統(tǒng)相似,對(duì)不同的消費(fèi)者毁兆,也有消費(fèi)者Group這樣的概念浙滤,不同的消費(fèi)組,可以消費(fèi)通一個(gè)消息气堕,對(duì)于不同的消費(fèi)組纺腊,都維護(hù)一個(gè)Idx下標(biāo),表示這一個(gè)消費(fèi)群組費(fèi)到了哪里茎芭,每次進(jìn)行消費(fèi)揖膜,都會(huì)更新一下這個(gè)下標(biāo),往后面一位進(jìn)行偏移梅桩。

其它

一壹粟、 跳躍表

跳躍表是一種有序數(shù)據(jù)結(jié)構(gòu),它通過在每個(gè)節(jié)點(diǎn)中維持多個(gè)指向其它節(jié)點(diǎn)的指針宿百,從而大道快速訪問節(jié)點(diǎn)的目的趁仙,具有以下性質(zhì):
1:有很多層結(jié)構(gòu)組成
2:每一層都是一個(gè)有序的鏈表,排列順序?yàn)橛筛叩降涂岩常贾辽侔瑑蓚€(gè)鏈表節(jié)點(diǎn)雀费,分別是前面的head節(jié)點(diǎn)和后面的nil節(jié)點(diǎn)
3:最底層的鏈表包含了所有的元素
4:如果一個(gè)元素出現(xiàn)在某一層的鏈表中,那么在該層之下的鏈表也全部都會(huì)出現(xiàn)
5:鏈表中的每個(gè)節(jié)點(diǎn)都包含兩個(gè)指針痊焊,一個(gè)指向同一層的下一個(gè)鏈表節(jié)點(diǎn)盏袄,另一個(gè)指向下一層的通一個(gè)鏈表節(jié)點(diǎn)

圖示:
image.png

Redis中跳躍表的定義如下:
typedef struct zskiplistNode {
  //層
  struct zskiplistLevel {
    //前進(jìn)指針
    struct zskiplistNode * forward;
    //跨度
    unsigned int span;
  } level[];
  //后退指針
  struct zskiplistNode * backward;
  //分值
  double score;
  //成員對(duì)象
  robj *obj;
} zskiplistNode

多個(gè)跳躍表節(jié)點(diǎn)構(gòu)成一個(gè)跳躍表

typedef struct zskiplist {
  //表頭節(jié)點(diǎn)和表尾節(jié)點(diǎn)
  struct zskiplistNode *header, *tail;
  //表中節(jié)點(diǎn)的數(shù)量
  unsigned long length;
  //表中層數(shù)最大的節(jié)點(diǎn)的層數(shù)
  int level;
}
跳表操作

1:搜索,從最高層的鏈表節(jié)點(diǎn)開始宋光,如果比當(dāng)前節(jié)點(diǎn)要大和比當(dāng)前層的下一個(gè)節(jié)點(diǎn)要小貌矿,那么則往下找炭菌,也及時(shí)和當(dāng)前層的下一層的節(jié)點(diǎn)下一個(gè)節(jié)點(diǎn)
2:插入罪佳,首先確定插入的層數(shù),有一種方法是拋一個(gè)硬幣黑低,如果是正面就累加赘艳,直到遇到反面為止,最后記錄正面的次數(shù)作為插入的層數(shù)克握,當(dāng)確定插入的層數(shù)K后蕾管,則需要將新元素插入從底層到K層
3:刪除,在各個(gè)層中找到包含指定值得節(jié)點(diǎn)菩暗,然后將節(jié)點(diǎn)從鏈表中刪除即可掰曾,如果刪除以后只剩下頭尾兩個(gè)節(jié)點(diǎn),則刪除這一層停团。

二旷坦、整數(shù)集合

整數(shù)集合是Redis用于保存整數(shù)值集合的抽象數(shù)據(jù)類型掏熬,它可以保存int16_t、int32_t秒梅、int64_t的整數(shù)值旗芬,并且保證集合中不會(huì)出現(xiàn)重復(fù)元素。

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

整數(shù)集合的每個(gè)元素都是contents數(shù)組的一個(gè)數(shù)據(jù)項(xiàng)捆蜀,他們按照從小到大的順序排列疮丛,并且不包含任何重復(fù)項(xiàng)。
length屬性記錄了contents數(shù)組的大小辆它。
需要注意的是雖然contents數(shù)組聲明為int8_t類型誊薄,但是實(shí)際上contents數(shù)組并不保存任何int8_t類型的值,其真正類型由encoding來決定锰茉。

  • 升級(jí)
    當(dāng)我們新增元素類型比原集合元素類型的長度要大時(shí)暇屋,需要對(duì)整數(shù)集合進(jìn)行升級(jí),才能將新元素放入整數(shù)集合中洞辣,具體步驟:
    1:根據(jù)新元素類型咐刨,擴(kuò)展整數(shù)集合底層數(shù)組的大小,并為新元素分配空間
    2:將底層數(shù)組現(xiàn)有的所有元素都轉(zhuǎn)成與新元素相同類型的元素扬霜,并將轉(zhuǎn)換后的元素放到正確的位置定鸟,放置過程中,維持整個(gè)元素順序都是有序的著瓶。
    3:將新元素添加到整數(shù)集合中(保證有序)
  • 降級(jí)
    整數(shù)集合不支持降級(jí)联予,一旦對(duì)數(shù)組進(jìn)行了升級(jí),編碼就會(huì)一直保存升級(jí)后的狀態(tài)

三材原、壓縮列表

壓縮列表(ziplist)是Redis為了節(jié)省內(nèi)存而開發(fā)的沸久,是由一系列特殊編碼的連續(xù)內(nèi)存塊組成的順序型數(shù)據(jù)結(jié)構(gòu),一個(gè)壓縮列表可以包含任意多個(gè)節(jié)點(diǎn)(entry)余蟹,每個(gè)節(jié)點(diǎn)可以保存一個(gè)字節(jié)數(shù)組或一個(gè)整數(shù)值卷胯。
壓縮列表的原理:壓縮列表并不是對(duì)數(shù)據(jù)利用某種算法進(jìn)行壓縮的,而是將數(shù)據(jù)按照一定規(guī)則編碼在一塊連續(xù)的內(nèi)存區(qū)域威酒,目的是節(jié)省內(nèi)存窑睁。


image.png

壓縮列表的每個(gè)節(jié)點(diǎn)構(gòu)成如下:


image.png

1、previous_entry_ength:記錄壓縮列表前一個(gè)字節(jié)的長度葵孤。previous_entry_ength的長度可能是1個(gè)字節(jié)或者是5個(gè)字節(jié)担钮,如果上一個(gè)節(jié)點(diǎn)的長度小于254,則該節(jié)點(diǎn)只需要一個(gè)字節(jié)就可以表示前一個(gè)節(jié)點(diǎn)的長度了尤仍,如果前一個(gè)節(jié)點(diǎn)的長度大于等于254箫津,則previous length的第一個(gè)字節(jié)為254,后面用四個(gè)字節(jié)表示當(dāng)前節(jié)點(diǎn)前一個(gè)節(jié)點(diǎn)的長度。利用此原理即當(dāng)前節(jié)點(diǎn)位置減去上一個(gè)節(jié)點(diǎn)的長度即得到上一個(gè)節(jié)點(diǎn)的起始位置苏遥,壓縮列表可以從尾部向頭部遍歷送挑。這么做很有效地減少了內(nèi)存的浪費(fèi)。
2暖眼、encoding:節(jié)點(diǎn)的encoding保存的是節(jié)點(diǎn)的content的內(nèi)容類型以及長度惕耕,encoding類型一共有兩種,一種字節(jié)數(shù)組一種是整數(shù)诫肠,encoding區(qū)域長度為1字節(jié)司澎、2字節(jié)或者5字節(jié)長。
3栋豫、content:content區(qū)域用于保存節(jié)點(diǎn)的內(nèi)容挤安,節(jié)點(diǎn)內(nèi)容類型和長度由encoding決定。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末丧鸯,一起剝皮案震驚了整個(gè)濱河市蛤铜,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌丛肢,老刑警劉巖围肥,帶你破解...
    沈念sama閱讀 211,743評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異蜂怎,居然都是意外死亡穆刻,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門杠步,熙熙樓的掌柜王于貴愁眉苦臉地迎上來氢伟,“玉大人,你說我怎么就攤上這事幽歼《渎啵” “怎么了?”我有些...
    開封第一講書人閱讀 157,285評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵甸私,是天一觀的道長诚些。 經(jīng)常有香客問我,道長颠蕴,這世上最難降的妖魔是什么泣刹? 我笑而不...
    開封第一講書人閱讀 56,485評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮犀被,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘外冀。我一直安慰自己寡键,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,581評(píng)論 6 386
  • 文/花漫 我一把揭開白布雪隧。 她就那樣靜靜地躺著西轩,像睡著了一般员舵。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上藕畔,一...
    開封第一講書人閱讀 49,821評(píng)論 1 290
  • 那天马僻,我揣著相機(jī)與錄音,去河邊找鬼注服。 笑死韭邓,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的溶弟。 我是一名探鬼主播女淑,決...
    沈念sama閱讀 38,960評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼辜御!你這毒婦竟也來了鸭你?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,719評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤擒权,失蹤者是張志新(化名)和其女友劉穎袱巨,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體碳抄,經(jīng)...
    沈念sama閱讀 44,186評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡瓣窄,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,516評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了纳鼎。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片俺夕。...
    茶點(diǎn)故事閱讀 38,650評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖贱鄙,靈堂內(nèi)的尸體忽然破棺而出劝贸,到底是詐尸還是另有隱情,我是刑警寧澤逗宁,帶...
    沈念sama閱讀 34,329評(píng)論 4 330
  • 正文 年R本政府宣布映九,位于F島的核電站,受9級(jí)特大地震影響瞎颗,放射性物質(zhì)發(fā)生泄漏件甥。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,936評(píng)論 3 313
  • 文/蒙蒙 一哼拔、第九天 我趴在偏房一處隱蔽的房頂上張望引有。 院中可真熱鬧,春花似錦倦逐、人聲如沸譬正。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,757評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽曾我。三九已至粉怕,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間抒巢,已是汗流浹背贫贝。 一陣腳步聲響...
    開封第一講書人閱讀 31,991評(píng)論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蛉谜,地道東北人稚晚。 一個(gè)月前我還...
    沈念sama閱讀 46,370評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像悦陋,于是被迫代替她去往敵國和親蜈彼。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,527評(píng)論 2 349

推薦閱讀更多精彩內(nèi)容

  • 1.數(shù)據(jù)結(jié)構(gòu) 1.1字符串 字符串類型的值實(shí)際可以是字符串俺驶、數(shù)字(整數(shù),浮點(diǎn)數(shù))暮现,甚至是二進(jìn)制(圖片还绘、視頻)...
    Sponge1128閱讀 1,235評(píng)論 0 0
  • 表情是什么,我認(rèn)為表情就是表現(xiàn)出來的情緒栖袋。表情可以傳達(dá)很多信息拍顷。高興了當(dāng)然就笑了,難過就哭了塘幅。兩者是相互影響密不可...
    Persistenc_6aea閱讀 124,458評(píng)論 2 7
  • 16宿命:用概率思維提高你的勝算 以前的我是風(fēng)險(xiǎn)厭惡者昔案,不喜歡去冒險(xiǎn),但是人生放棄了冒險(xiǎn)电媳,也就放棄了無數(shù)的可能踏揣。 ...
    yichen大刀閱讀 6,041評(píng)論 0 4