一、基礎
1、redis字典數據庫的KV鍵值對到底是什么?
redis 是 key-value 存儲系統,其中key類型一般為字符串,value 類型則為redis對象(redisObject)。

從C的的源碼分析KV是什么,每個鍵值對都會有一個dictEntry。


Redis定義了redisObjec結構體,來表示string、hash、list、set、zset等數據類型。
RedisObject +Redis數據類型+Redis 所有編碼方式(底層實現)三者之間的關系,如下圖

2、從set hello world說起
set hello word為例,因為Redis是KV鍵值對的數據庫,每個鍵值對都會有一個dictEntry(源碼位置:dict.h),里面指向了key和value的指針,next 指向下一個 dictEntry。
key 是字符串,但是 Redis 沒有直接使用 C 的字符數組,而是存儲在redis自定義的 SDS中。
value 既不是直接作為字符串存儲,也不是直接存儲在 SDS 中,而是存儲在redisObject 中。
實際上五種常用的數據類型的任何一種,都是通過 redisObject 來存儲的。


操作命令
- 查看l類型:type key
- 查看編碼:object encoding key
- debug結構:debug object key
3、redisObjec結構的作用

為了便於操作,Redis采用 redisObjec 結構來統一五種不同的數據類型,這樣所有的數據類型就都可以以相同的形式在函數間傳遞而不用使用特定的類型結構。同時,為了識別不同的數據類型,redisObjec中定義了type和encoding字段對不同的數據類型加以區別。簡單地說,redisObjec就是string、hash、list、set、zset的父類,可以在函數間傳遞時隱藏具體的類型信息,所以作者抽象了redisObjec結構來到達同樣的目的。
各字段的含義:

- 4位的type表示具體的數據類型
- 4位的encoding表示該類型的物理編碼方式,同一種數據類型可能有不同的編碼方式。(比如String就提供了3種:int embstr raw)
- lru字段表示當內存超限時采用LRU算法清除內存中的對象。
- refcount表示對象的引用計數。
- ptr指針指向真正的底層數據結構的指針。
4、案例分析 set age 17
set age 17

| type | 類型,當前string |
| encoding | 編碼,此處是數字類型int |
| lru | 最近被訪問的時間 |
| refcount | 等於1,表示當前對象被引用的次數 |
| ptr | value值是多少,當前是17 |
二、String數據結構介紹
1、3大編碼格式
(1)int
保存long 型(長整型)的64位(8個字節)有符號整數。范圍:-2^63 ~ 2^63-1。
只有整數才會使用 int,如果是浮點數, Redis 內部其實先將浮點數轉化為字符串值,然后再保存。
(2)embstr
EMBSTR 顧名思義即:embedded string,表示嵌入式的String。
代表 embstr 格式的 SDS(Simple Dynamic String 簡單動態字符串),保存長度小於44字節的字符串。
(3)raw
保存長度大於44字節的字符串。
2、SDS
Redis沒有直接復用C語言的字符串,而是新建了屬於自己的結構-----SDS。
在Redis數據庫里,包含字符串值的鍵值對都是由SDS實現的(Redis中所有的鍵都是由字符串對象實現的即底層是由SDS實現,Redis中所有的值對象中包含的字符串對象底層也是由SDS實現)。

Redis中字符串的實現,SDS有多種結構(sds.h):
- sdshdr5、(2^5=32byte)(作者測試,正常不使用)
- sdshdr8、(2 ^ 8=256byte)
- sdshdr16、(2 ^ 16=65536byte=64KB)
- sdshdr32、 (2 ^ 32byte=4GB)
- sdshdr64,2的64次方byte=17179869184G用於存儲不同的長度的字符串。
len:表示 SDS 的長度,使我們在獲取字符串長度的時候可以在 O(1)情況下拿到,而不是像 C 那樣需要遍歷一遍字符串。
alloc:可以用來計算 free 就是字符串已經分配的未使用的空間,有了這個值就可以引入預分配空間的算法了,而不用去考慮內存分配的問題。
buf:表示字符串數組,真存數據的。
3、Redis為什么重新設計一個 SDS 數據結構 不使用C的?
C語言沒有Java里面的String類型,只能是靠自己的char[]來實現,字符串在 C 語言中的存儲方式,想要獲取 「Redis」的長度,需要從頭開始遍歷,直到遇到 '\0' 為止。所以,Redis 沒有直接使用 C 語言傳統的字符串標識,而是自己構建了一種名為簡單動態字符串 SDS(simple dynamic string)的抽象類型,並將 SDS 作為 Redis 的默認字符串。

| C語言 | SDS | |
| 字符串長度處理 | 需要從頭開始遍歷,直到遇到 '\0' 為止,時間復雜度O(N) | 記錄當前字符串的長度,直接讀取即可,時間復雜度 O(1) |
| 內存重新分配 | 分配內存空間超過后,會導致數組下標越級或者內存分配溢出 | 空間預分配 SDS 修改后,len 長度小於 1M,那么將會額外分配與 len 相同長度的未使用空間。如果修改后長度大於 1M,那么將分配1M的使用空間。 惰性空間釋放 有空間分配對應的就有空間釋放。SDS 縮短時並不會回收多余的內存空間,而是使用 free 字段將多出來的空間記錄下來。如果后續有變更操作,直接使用 free 中記錄的空間,減少了內存的分配。 |
| 二進制安全 | 二進制數據並不是規則的字符串格式,可能會包含一些特殊的字符,比如 '\0' 等。前面提到過,C中字符串遇到 '\0' 會結束,那 '\0' 之后的數據就讀取不上了 | 根據 len 長度來判斷字符串結束的,二進制安全的問題就解決了 |
4、INT 編碼格式
Redis 啟動時會預先建立 10000 個分別存儲 0~9999 的 redisObject 變量作為共享對象,這就意味着如果 set字符串的鍵值在 0~10000 之間的話,則可以 直接指向共享對象 而不需要再建立新對象,此時鍵值不占空間!
set k1 123
set k2 123


5、EMBSTR編碼格式
對於長度小於 44的字符串,Redis 對鍵值采用OBJ_ENCODING_EMBSTR 方式,EMBSTR 顧名思義即:embedded string,表示嵌入式的String。從內存結構上來講 即字符串 sds結構體與其對應的 redisObject 對象分配在同一塊連續的內存空間,字符串sds嵌入在redisObject對象之中一樣。


6、RAW 編碼格式
當字符串的鍵值為長度大於44的超長字符串時,Redis 則會將鍵值的內部編碼方式改為OBJ_ENCODING_RAW格式,這與OBJ_ENCODING_EMBSTR編碼方式的不同之處在於,此時動態字符串sds的內存與其依賴的redisObject的內存不再連續了。
7、總結
(1)明明沒有超過閾值,為什么變成 raw 了

(2)轉變邏輯圖

三、Hash數據結構介紹
1、基礎
- hash-max-ziplist-entries:使用壓縮列表保存時哈希集合中的最大元素個數(默認512)。
- hash-max-ziplist-value:使用壓縮列表保存時哈希集合中單個元素的最大長度(默認64)。
Hash類型鍵的字段個數 小於 hash-max-ziplist-entries 並且每個字段名和字段值的長度 小於 hash-max-ziplist-value 時,Redis才會使用 OBJ_ENCODING_ZIPLIST來存儲該鍵,前述條件任意一個不滿足則會轉換為 OBJ_ENCODING_HT的編碼方式。
ziplist升級到hashtable可以,反過來降級不可以。

2、ziplist 編碼格式
(1)ziplist 基礎結構
ziplist是一個經過特殊編碼的雙向鏈表,它不存儲指向上一個鏈表節點和指向下一個鏈表節點的指針,而是存儲上一個節點長度和當前節點長度,通過犧牲部分讀寫性能,來換取高效的內存空間利用率,節約內存,是一種時間換空間的思想。只用在字段個數少,字段值小的場景里面。本質上是字節數組的一系列特殊編碼的連續內存塊組成的順序型數據結構。

zlbytes 4字節,記錄整個壓縮列表占用的內存字節數。
zltail 4字節,記錄壓縮列表表尾節點的位置。
zllen 2字節,記錄壓縮列表節點個數。
zlentry 列表節點,長度不定,由內容決定。
zlend 1字節,0xFF 標記壓縮的結束。
(2)zlentry結構
前節點:(前節點占用的內存字節數)表示前1個zlentry的長度,prev_len有兩種取值情況:1字節或5字節。取值1字節時,表示上一個entry的長度小於254字節。雖然1字節的值能表示的數值范圍是0到255,但是壓縮列表中zlend的取值默認是255,因此,就默認用255表示整個壓縮列表的結束,其他表示長度的地方就不能再用255這個值了。所以,當上一個entry長度小於254字節時,prev_len取值為1字節,否則,就取值為5字節。
enncoding:記錄節點的content保存數據的類型和長度。
content:保存實際數據內容
(3)壓縮列表的遍歷
通過指向表尾節點的位置指針p1, 減去節點的previous_entry_length,得到前一個節點起始地址的指針。如此循環,從表尾遍歷到表頭節點。從表尾向表頭遍歷操作就是使用這一原理實現的,只要我們擁有了一個指向某個節點起始地址的指針,那么通過這個指針以及這個節點的previous_entry_length屬性程序就可以一直向前一個節點回溯,最終到達壓縮列表的表頭節點。 
(4)明明有鏈表了,為什么出來一個壓縮鏈表?
- 普通的雙向鏈表會有兩個指針,在存儲數據很小的情況下,我們存儲的實際數據的大小可能還沒有指針占用的內存大,得不償失。ziplist 是一個特殊的雙向鏈表沒有維護雙向指針:prev next;而是存儲上一個 entry的長度和 當前entry的長度,通過長度推算下一個元素在什么地方。犧牲讀取的性能,獲得高效的存儲空間,因為(簡短字符串的情況)存儲指針比存儲entry長度更費內存。這是典型的“時間換空間”。
- 鏈表在內存中一般是不連續的,遍歷相對比較慢,而ziplist可以很好的解決這個問題,普通數組的遍歷是根據數組里存儲的數據類型找到下一個元素的(例如int類型的數組訪問下一個元素時每次只需要移動一個sizeof(int)就行),但是ziplist的每個節點的長度是可以不一樣的,而我們面對不同長度的節點又不可能直接sizeof(entry),所以ziplist只好將一些必要的偏移量信息記錄在了每一個節點里,使之能跳到上一個節點或下一個節點。
- 頭節點里有頭節點里同時還有一個參數 len,和string類型提到的 SDS 類似,這里是用來記錄鏈表長度的。因此獲取鏈表長度時不用再遍歷整個鏈表,直接拿到len值就可以了,這個時間復雜度是 O(1)
3、hashtable 編碼格式
在 Redis 中,hashtable 被稱為字典(dictionary),它是一個數組+鏈表的結構。
OBJ_ENCODING_HT 這種編碼方式內部才是真正的哈希表結構,或稱為字典結構,其可以實現O(1)復雜度的讀寫操作,因此效率很高。在 Redis內部,從 OBJ_ENCODING_HT類型到底層真正的散列表數據結構是一層層嵌套下去的,組織關系見面圖:


源代碼:dict.h

四、List數據結構介紹
1、基礎
(1)ziplist中entry配置:list-max-ziplist-size,當取正值的時候,表示按照數據項個數來限定每個quicklist節點上的ziplist長度。比如,當這個參數配置成5的時候,表示每個quicklist節點的ziplist最多包含5個數據項。當取負值的時候,表示按照占用字節數來限定每個quicklist節點上的ziplist長度。這時,它只能取-1到-5這五個值:
- -5: 每個quicklist節點上的ziplist大小不能超過64 Kb。(注:1kb => 1024 bytes)
- -4: 每個quicklist節點上的ziplist大小不能超過32 Kb。
- -3: 每個quicklist節點上的ziplist大小不能超過16 Kb。
- -2: 每個quicklist節點上的ziplist大小不能超過8 Kb。(-2是Redis給出的默認值)
- -1: 每個quicklist節點上的ziplist大小不能超過4 Kb。
(2)ziplist壓縮配置:list-compress-depth,表示一個quicklist兩端不被壓縮的節點個數。這里的節點是指quicklist雙向鏈表的節點,而不是指ziplist里面的數據項個數:
- 0: 是個特殊值,表示都不壓縮。這是Redis的默認值。
- 1: 表示quicklist兩端各有1個節點不壓縮,中間的節點壓縮。
- 2: 表示quicklist兩端各有2個節點不壓縮,中間的節點壓縮。
- 3: 表示quicklist兩端各有3個節點不壓縮,中間的節點壓縮。
- 依此類推…
2、結構
list用quicklist來存儲,quicklist 存儲了一個雙向鏈表,每個節點都是一個 ziplist。
在低版本的Redis中,list采用的底層數據結構是ziplist+linkedList;高版本的Redis中底層數據結構是quicklist(它替換了ziplist+linkedList),而quicklist也用到了ziplist。

3、quicklist
quicklist 實際上是 zipList 和 linkedList 的混合體,它將 linkedList按段切分,每一段使用 zipList 來緊湊存儲,多個 zipList 之間使用雙向指針串接起來。

4、quickNode

五、Set數據結構介紹
Redis 用 intset 或 hashtable 存儲set。如果元素都是整數類型,就用intset存儲。如果不是整數類型,就用hashtable(數組+鏈表的存來儲結構)。key就是元素的值,value為null。

六、ZSet數據結構介紹
1、基礎
當有序集合中包含的元素數量超過服務器屬性 server.zset_max_ziplist_entries 的值(默認值為 128 ),或者有序集合中新添加元素的 member 的長度大於服務器屬性 server.zset_max_ziplist_value 的值(默認值為 64 )時,redis會使用跳躍表作為有序集合的底層實現。否則會使用ziplist作為有序集合的底層實現。

2、skiplist 跳表
skiplist是一種以空間換取時間的結構。由於鏈表,無法進行二分查找,因此借鑒數據庫索引的思想,提取出鏈表中關鍵節點(索引),先在關鍵節點上查找,再進入下層鏈表查找。提取多層關鍵節點,就形成了跳躍表。
跳表 = 鏈表 + 多級索引

3、跳表的時間復雜度
首先每一級索引我們提升了2倍的跨度,那就是減少了2倍的步數,所以是n/2、n/4、n/8以此類推:
第 k 級索引結點的個數就是 n/(2^k);
假設索引有 h 級, 最高的索引有2個結點;n/(2^h) = 2, 從這個公式我們可以求得 h = log2(N)-1;
所以最后得出跳表的時間復雜度是O(logN)
4、跳表的空間復雜度
首先原始鏈表長度為n,
如果索引是每2個結點有一個索引結點,每層索引的結點數:n/2, n/4, n/8 ... , 8, 4, 2 以此類推;
或者所以是每3個結點有一個索引結點,每層索引的結點數:n/3, n/9, n/27 ... , 9, 3, 1 以此類推;
所以空間復雜度是O(n)
5、跳表的優缺點
跳表是一個最典型的空間換時間解決方案,而且只有在數據量較大的情況下才能體現出來優勢。而且應該是讀多寫少的情況下才能使用,所以它的適用范圍應該還是比較有限的。
維護成本相對要高:新增或者刪除時需要把所有索引都更新一遍。
最后在新增和刪除的過程中的更新,時間復雜度也是O(log n)
七、總結
1、不同數據類型對應的底層數據結構
(1)String 字符串
int:8個字節的長整型。
embstr:小於等於44個字節的字符串。
raw:大於44個字節的字符串。
Redis會根據當前值的類型和長度決定使用哪種內部編碼實現。
(2)Hash 哈希
ziplist(壓縮列表):當哈希類型元素個數小於hash-max-ziplist-entries 配置(默認512個)、同時所有值都小於hash-max-ziplist-value配置(默認64 字節)時,Redis會使用ziplist作為哈希的內部實現,ziplist使用更加緊湊的 結構實現多個元素的連續存儲,所以在節省內存方面比hashtable更加優秀。
hashtable(哈希表):當哈希類型無法滿足ziplist的條件時,Redis會使 用hashtable作為哈希的內部實現,因為此時ziplist的讀寫效率會下降,而hashtable的讀寫時間復雜度為O(1)。
(3)list 列表
ziplist(壓縮列表):當列表的元素個數小於list-max-ziplist-entries配置 (默認512個),同時列表中每個元素的值都小於list-max-ziplist-value配置時 (默認64字節),Redis會選用ziplist來作為列表的內部實現來減少內存的使用。
linkedlist(鏈表):當列表類型無法滿足ziplist的條件時,Redis會使用 linkedlist作為列表的內部實現。quicklist ziplist和linkedlist的結合以ziplist為節點的鏈表(linkedlist)
(4)set 集合
intset(整數集合):當集合中的元素都是整數且元素個數小於set-max- intset-entries配置(默認512個)時,Redis會選用intset來作為集合的內部實現,從而減少內存的使用。
hashtable(哈希表):當集合類型無法滿足intset的條件時,Redis會使用hashtable作為集合的內部實現。
(5)zset 有序集合
ziplist(壓縮列表):當有序集合的元素個數小於zset-max-ziplist- entries配置(默認128個),同時每個元素的值都小於zset-max-ziplist-value配 置(默認64字節)時,Redis會用ziplist來作為有序集合的內部實現,ziplist 可以有效減少內存的使用。
skiplist(跳躍表):當ziplist條件不滿足時,有序集合會使用skiplist作 為內部實現,因為此時ziplist的讀寫效率會下降。
2、redis數據類型以及數據結構的時間復雜度

