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

歡迎訪問 生活随笔!

生活随笔

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

数据库

浅谈Redis五种数据结构的底层原理

發(fā)布時間:2023/12/4 数据库 51 豆豆
生活随笔 收集整理的這篇文章主要介紹了 浅谈Redis五种数据结构的底层原理 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

概念

Redis作為一個開源的用C編寫的非關(guān)系型數(shù)據(jù)庫,基于優(yōu)秀的CRUD效率,常用于軟件系統(tǒng)的緩存,其本身提供了以下五種數(shù)據(jù)格式:

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

接下來我們就要針對這五種數(shù)據(jù)結(jié)構(gòu),來分析其底層的結(jié)構(gòu)
這里選用的版本是redis-5.0.4,所以可能有很多地方和如今網(wǎng)絡(luò)上的其他博文不太一致,不同的地方我會在文中指出
string
因為redis使用c語言開發(fā),所以自然沒有java和c++的那些字符串類庫,在redis中,其自己定義了一種字符串格式,叫做SDS(Simple Dynamic String),即簡單動態(tài)字符串
這個結(jié)構(gòu)定義在sds.h中:

typedef char *sds;

但是這個sds類型僅作為參數(shù)和返回值使用,并不是真正用于操作的類型,真正核心的部分是下面的這些類:

struct __attribute__ ((__packed__)) sdshdr5 {unsigned char flags; char buf[]; }; struct __attribute__ ((__packed__)) sdshdr8 {uint8_t len; uint8_t alloc; unsigned char flags; char buf[]; }; struct __attribute__ ((__packed__)) sdshdr16 {uint16_t len;uint16_t alloc; unsigned char flags;char buf[]; }; struct __attribute__ ((__packed__)) sdshdr32 {uint32_t len;uint32_t alloc; unsigned char flags; char buf[]; }; struct __attribute__ ((__packed__)) sdshdr64 {uint64_t len; uint64_t alloc;unsigned char flags; char buf[]; };

除掉第一個結(jié)構(gòu)體(已經(jīng)棄用),sds具體類型的結(jié)構(gòu)可以分為以下部分:

  • len:已使用的長度,即字符串的真實長度
  • alloc:除去標(biāo)頭和終止符(’\0’)后的長度
  • flags:低3位表示字符串類型,其余5位未使用(我暫時沒發(fā)現(xiàn)redis在哪里使用過這個屬性)
  • buf[]:存儲字符數(shù)據(jù)

這里和老版本做一下對比,因為我手頭只有4.x和5.x的版本,它們sds的實現(xiàn)是一致的,但是據(jù)其他人說sds之前的版本實現(xiàn)方式不同,有時間我會去下載下來看一下,其將字符串分為以下部分:

  • len:buf中已經(jīng)占有的長度(表示此字符串的實際長度)
  • free:buf中未使用的緩沖區(qū)長度
  • buf[]:實際保存字符串?dāng)?shù)據(jù)的地方

redis同時寫重寫了大量的與sds類型相關(guān)的方法,那redis為什么要這么下功夫呢,有以下4個優(yōu)點:

  • 降低獲取字符串長度的時間復(fù)雜度到O(1)
  • 減少了修改字符串時的內(nèi)存重分配次數(shù)
  • 兼容c字符串的同時,提高了一些字符串工具方法的效率
  • 二進制安全(數(shù)據(jù)寫入的格式和讀取的格式一致)

list
我們查看源文件可以看到有兩個list,一個是ziplist,字面意是壓縮列表,另一個是quicklist,字面意是快速列表,在redis中直接使用的是quicklist,但是我們先來看ziplist
ziplist
ziplist并不是一個類名,其結(jié)構(gòu)是下面這樣的: …
其中各部分代表的含義如下:

  • zlbytes:4個字節(jié)(32bits),表示ziplist占用的總字節(jié)數(shù)
  • zltail:4個字節(jié)(32bits),表示ziplist中最后一個節(jié)點在ziplist中的偏移字節(jié)數(shù)
  • entries:2個字節(jié)(16bits),表示ziplist中的元素數(shù) entry:長度不定,表示ziplist中的數(shù)據(jù)
  • zlend:1個字節(jié)(8bits),表示結(jié)束標(biāo)記,這個值固定為ff(255)

這些數(shù)據(jù)均為小端存儲,所以可能有些人查看數(shù)據(jù)的二進制流與其含義對應(yīng)不上,其實是因為讀數(shù)據(jù)的方式錯了
ziplist內(nèi)部采取數(shù)據(jù)壓縮的方式進行存儲,壓縮方式就不是重點了,我們僅從宏觀來看,ziplist類似一個封裝的數(shù)組,通過zltail可以方便地進行追加和刪除尾部數(shù)據(jù)、使用entries可以方便地計算長度
但是其依然有數(shù)組的缺點,就是當(dāng)插入和刪除數(shù)據(jù)時會頻繁地引起數(shù)據(jù)移動,所以就引出了quicklist數(shù)據(jù)類型
quicklist
其核心數(shù)據(jù)結(jié)構(gòu)如下:

typedef struct quicklist {quicklistNode *head;quicklistNode *tail;unsigned long count; /* ziplist所有節(jié)點的個數(shù) */unsigned long len; /* quicklistNode節(jié)點的個數(shù) */int fill : 16; /* 單個節(jié)點的填充因子 */unsigned int compress : 16; /* 壓縮端結(jié)點的深度 */ } quicklist;

我們可以明顯地看出,quicklist是一個雙向鏈表的結(jié)構(gòu),但是內(nèi)部又涉及了ziplist,我們可以這么說,在宏觀上,quicklist是一個雙向鏈表,在微觀上,每一個quicklist的節(jié)點都是一個ziplist
在redis.conf中,可以使用下面兩個參數(shù)來進行優(yōu)化:

  • list-max-ziplist-size:表示每個quicklistNode的字節(jié)大小。默認為2,表示8KB
  • list-compress-depth:表示quicklistNode節(jié)點是否要壓縮。默認為0,表示不壓縮

這種存儲方式的優(yōu)點和鏈表的優(yōu)點一致,就是插入和刪除的效率很高,而鏈表查詢的效率又由ziplist來進行彌補,所以quicklist就成為了list數(shù)據(jù)結(jié)構(gòu)的首選
hash
hash這種結(jié)構(gòu)在redis的使用時最為常見,在redis中,hash這種結(jié)構(gòu)有兩種表示:zipmap和dict
zipmap
zipmap其格式形如下面這樣: <zmlen><len>"foo"<len><free>"bar"<len>"hello"<len><free>"world"
各部分的含義如下:

  • zmlen:1個字節(jié),表示zipmap的總字節(jié)數(shù)
  • len:1~5個字節(jié),表示接下來存儲的字符串長度
  • free:1個字節(jié),是一個無符號的8位數(shù),表示字符串后面的空閑未使用字節(jié)數(shù),由于修改與鍵對應(yīng)的值而產(chǎn)生

這其中相鄰的兩個字符串就分別是鍵和值,比如在上面的例子中,就表示"foo" => "bar", "hello" => "world"這樣的對應(yīng)關(guān)系

這種方式的缺點也很明顯,就是查找的時間復(fù)雜度為O(n),所以只能當(dāng)作一個輕量級的hashmap來使用
dict
這種方式就適于存儲大規(guī)模的數(shù)據(jù),其格式如下:

typedef struct dict {dictType *type;/* 指向自定義類型的指針,可以存儲各類型數(shù)據(jù) */void *privdata; /* 私有數(shù)據(jù)的指針 */dictht ht[2];/* 兩個hash表,一般只有h[0]有效,h1[1]只在rehash的時候才有值 */long rehashidx; /* -1:沒有在rehash的過程中,大于等于0:表示執(zhí)行rehash到第幾步 */unsigned long iterators; /* 正在遍歷的迭代器個數(shù) */ } dict;

如果我們不想更深入的話了解到這種程度就可以了,其中真正存儲數(shù)據(jù)的是dictEntry結(jié)構(gòu),如下:

typedef struct dictEntry {void *key;union {void *val;uint64_t u64;int64_t s64;double d;} v;struct dictEntry *next; } dictEntry;

很明顯是一個鏈表,我們知道這是采用鏈?zhǔn)浇Y(jié)構(gòu)存儲就足夠了
這種方式會消耗較多的內(nèi)存,所以一般數(shù)據(jù)較少時會采用輕量級的zipmap
set
在redis中,我們可以查看intset.h文件,這是一個存儲整數(shù)的集合,其結(jié)構(gòu)如下:

typedef struct intset {uint32_t encoding;uint32_t length;int8_t contents[]; } intset;

其中各字段含義如下:

  • encoding:數(shù)據(jù)編碼格式,表示每個數(shù)據(jù)元素用幾個字節(jié)存儲(可取的值有2、4,和8)
  • length:元素個數(shù)
  • contents:柔性數(shù)組,這部分內(nèi)存單獨分配,不包含在intset中

具體的操作我們就不詳細展開了,了解集合這種數(shù)據(jù)結(jié)構(gòu)的應(yīng)該都很清楚,我們這里說一下,intset有一個數(shù)據(jù)升級的概念,比方說我們有一個16位整數(shù)的set,這時候插入了一個32位整數(shù),所以就導(dǎo)致整個集合都升級為32位整數(shù),但是反過來卻不行,這也就是柔性數(shù)組的由來
如果集合過大,會采用dict的方式來進行存儲
zset
zset,有很多地方也叫做sorted set,是一個鍵值對的結(jié)構(gòu),其鍵被稱為member,也就是集合元素(zset依然是set,所以member不能相同),其對應(yīng)的值被稱為score,是一個浮點數(shù),可以理解為優(yōu)先級,用于排列zset的順序
其也有兩種存儲方式,一種是ziplist/zipmap的格式,這種方式我們就不過多介紹了,只需要了解這種格式將數(shù)據(jù)按照score的順序排列即可
另一種存儲格式是采用了skiplist,意為跳躍表,可以看成平衡樹映射的數(shù)組,其查找的時間復(fù)雜度和平衡樹基本沒有差別,但是實現(xiàn)更為簡單,形如下面這樣的結(jié)構(gòu)(圖來源跳躍表的原理):

總結(jié)

以上是生活随笔為你收集整理的浅谈Redis五种数据结构的底层原理的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

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