Redis做爲一個開源的用C編寫的非關係型數據庫,基於優秀的CRUD效率,經常使用於軟件系統的緩存,其自己提供瞭如下五種數據格式:java
string:字符串c++
list:列表面試
hash:散列表redis
set:無序集合數據庫
zset:有序集合數組
接下來咱們就要針對這五種數據結構,來分析其底層的結構緩存
這裏選用的版本是redis-5.0.4
,因此可能有不少地方和現在網絡上的其餘博文不太一致,不一樣的地方我會在文中指出安全
由於redis使用c語言開發,因此天然沒有java和c++的那些字符串類庫,在redis中,其本身定義了一種字符串格式,叫作SDS(Simple Dynamic String),即簡單動態字符串網絡
這個結構定義在sds.h中:數據結構
typedef char *sds;
可是這個sds類型僅做爲參數和返回值使用,並非真正用於操做的類型,真正核心的部分是下面的這些類:
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[]; };
除掉第一個結構體(已經棄用),sds具體類型的結構能夠分爲如下部分:
len:已使用的長度,即字符串的真實長度
alloc:除去標頭和終止符('\0')後的長度
flags:低3位表示字符串類型,其他5位未使用(我暫時沒發現redis在哪裏使用過這個屬性)
buf[]:存儲字符數據
這裏和老版本作一下對比,由於我手頭只有4.x和5.x的版本,它們sds的實現是一致的,可是據其餘人說sds以前的版本實現方式不一樣,有時間我會去下載下來看一下,其將字符串分爲如下部分:
len:buf中已經佔有的長度(表示此字符串的實際長度)
free:buf中未使用的緩衝區長度
buf[]:實際保存字符串數據的地方
redis同時寫重寫了大量的與sds類型相關的方法,那redis爲何要這麼下功夫呢,有如下4個優勢:
下降獲取字符串長度的時間複雜度到O(1)
減小了修改字符串時的內存重分配次數
兼容c字符串的同時,提升了一些字符串工具方法的效率
二進制安全(數據寫入的格式和讀取的格式一致)
咱們查看源文件能夠看到有兩個list,一個是ziplist,字面意是壓縮列表,另外一個是quicklist,字面意是快速列表,在redis中直接使用的是quicklist,可是咱們先來看ziplist
ziplist並非一個類名,其結構是下面這樣的: <zlbytes><zltail><entries><entry>...<entry><zlend>
其中各部分表明的含義以下:
zlbytes:4個字節(32bits),表示ziplist佔用的總字節數
zltail:4個字節(32bits),表示ziplist中最後一個節點在ziplist中的偏移字節數
entries:2個字節(16bits),表示ziplist中的元素數
entry:長度不定,表示ziplist中的數據
zlend:1個字節(8bits),表示結束標記,這個值固定爲ff(255)
這些數據均爲小端存儲,因此可能有些人查看數據的二進制流與其含義對應不上,實際上是由於讀數據的方式錯了
ziplist內部採起數據壓縮的方式進行存儲,壓縮方式就不是重點了,咱們僅從宏觀來看,ziplist相似一個封裝的數組,經過zltail能夠方便地進行追加和刪除尾部數據、使用entries能夠方便地計算長度
可是其依然有數組的缺點,就是當插入和刪除數據時會頻繁地引發數據移動,因此就引出了quicklist數據類型
其核心數據結構以下:
typedef struct quicklist { quicklistNode *head; quicklistNode *tail; unsigned long count; /* ziplist全部節點的個數 */ unsigned long len; /* quicklistNode節點的個數 */ int fill : 16; /* 單個節點的填充因子 */ unsigned int compress : 16; /* 壓縮端結點的深度 */ } quicklist;
咱們能夠明顯地看出,quicklist是一個雙向鏈表的結構,可是內部又涉及了ziplist,咱們能夠這麼說,在宏觀上,quicklist是一個雙向鏈表,在微觀上,每個quicklist的節點都是一個ziplist
在redis.conf中,可使用下面兩個參數來進行優化:
list-max-ziplist-size:表示每一個quicklistNode的字節大小。默認爲2,表示8KB
list-compress-depth:表示quicklistNode節點是否要壓縮。默認爲0,表示不壓縮
這種存儲方式的優勢和鏈表的優勢一致,就是插入和刪除的效率很高,而鏈表查詢的效率又由ziplist來進行彌補,因此quicklist就成爲了list數據結構的首選
hash這種結構在redis的使用時最爲常見,在redis中,hash這種結構有兩種表示:zipmap和dict
zipmap其格式形以下面這樣: <zmlen><len>"foo"<len><free>"bar"<len>"hello"<len><free>"world"
各部分的含義以下:
zmlen:1個字節,表示zipmap的總字節數
len:1~5個字節,表示接下來存儲的字符串長度
free:1個字節,是一個無符號的8位數,表示字符串後面的空閒未使用字節數,因爲修改與鍵對應的值而產生
這其中相鄰的兩個字符串就分別是鍵和值,好比在上面的例子中,就表示"foo" => "bar", "hello" => "world"
這樣的對應關係
這種方式的缺點也很明顯,就是查找的時間複雜度爲O(n),因此只能看成一個輕量級的hashmap來使用
這種方式就適於存儲大規模的數據,其格式以下:
typedef struct dict { dictType *type;/* 指向自定義類型的指針,能夠存儲各種型數據 */ void *privdata; /* 私有數據的指針 */ dictht ht[2];/* 兩個hash表,通常只有h[0]有效,h1[1]只在rehash的時候纔有值 */ long rehashidx; /* -1:沒有在rehash的過程當中,大於等於0:表示執行rehash到第幾步 */ unsigned long iterators; /* 正在遍歷的迭代器個數 */ } dict;
若是咱們不想更深刻的話瞭解到這種程度就能夠了,其中真正存儲數據的是dictEntry結構,以下:
typedef struct dictEntry { void *key; union { void *val; uint64_t u64; int64_t s64; double d; } v; struct dictEntry *next; } dictEntry;
很明顯是一個鏈表,咱們知道這是採用鏈式結構存儲就足夠了
這種方式會消耗較多的內存,因此通常數據較少時會採用輕量級的zipmap
在redis中,咱們能夠查看intset.h
文件,這是一個存儲整數的集合,其結構以下:
typedef struct intset { uint32_t encoding; uint32_t length; int8_t contents[]; } intset;
其中各字段含義以下:
encoding:數據編碼格式,表示每一個數據元素用幾個字節存儲(可取的值有二、4,和8)
length:元素個數
contents:柔性數組,這部份內存單獨分配,不包含在intset中
具體的操做咱們就不詳細展開了,瞭解集合這種數據結構的應該都很清楚,咱們這裏說一下,intset有一個數據升級的概念,比方說咱們有一個16位整數的set,這時候插入了一個32位整數,因此就致使整個集合都升級爲32位整數,可是反過來卻不行,這也就是柔性數組的由來
若是集合過大,會採用dict的方式來進行存儲
zset,有不少地方也叫作sorted set,是一個鍵值對的結構,其鍵被稱爲member,也就是集合元素(zset依然是set,因此member不能相同),其對應的值被稱爲score,是一個浮點數,能夠理解爲優先級,用於排列zset的順序
其也有兩種存儲方式,一種是ziplist/zipmap的格式,這種方式咱們就不過多介紹了,只須要了解這種格式將數據按照score的順序排列便可
另外一種存儲格式是採用了skiplist,意爲跳躍表,能夠當作平衡樹映射的數組,其查找的時間複雜度和平衡樹基本沒有差異,可是實現更爲簡單,形以下面這樣的結構(圖來源跳躍表的原理):