2. 淺析Redis底層數據結構

概要
1)Redis中的字符串-sds
2)Redis中的HashMap-dict
3)dict的漸進式rehash
4)Redis的5種對象底層剖析

2.1 Redis中的字符串-sds

眾所周知,Redis是使用C語言實現的,C語言底層存放string是使用char數組,并且在最后會加上'\0'作為string的結束標志。

C語言存放"Vander" - 》 char[] str = "Vander\0"(此時字符串的length為6byte,但是占據7byte的空間,‘\0’不算在字符串長度里)

問題一:為什么Redis不直接跟C語言一樣存放string?

答:因為如果我想存放'Vander\0Jason'就會產生誤判,提前結束,這樣只會讀取到“Vander”,后面的“Jason”就被截取掉了。

由于Redis的Key都是字符串,所以先查看Redis底層的數據結構sds

問題二:如何減少C語言中的string修改字符串時帶來的內存重分配

C字符串的內存重分配.png

由于C字符串不記錄長度,所以每次增減字符都要對保存的C字符串的數組進行內存重分配

Redis為了解決增加字符數,頻繁重分配的問題,會進行內存預分配,來減少由于再次增加字符而導致的內存重分配的問題。即,假設原有sds存放char[] = 'V','a','n','d','e','r','\0',需要存放"Vander&Jason",由于此字符串占12byte,所以會預分配25byte(12*2+1,'\0'占1byte)。但是在大于1MB之后,每次新增不會按照兩倍擴展了,只能會再分配多1MB。

惰性空間釋放.png

為了解決縮少字符數,導致的內存泄漏的問題,通過調整free來釋放惰性空間,通過free來說明當前數組還有剩余空間可以使用。

從Redis設計的sds來看,有三個特點:
1)二進制安全的數據結構(不直接用'\0'作為判斷結束的標志,而是加上len的標識)
2)提供內存預分配機制,避免了頻繁的內存分配
3)兼容C語言的函數庫(數組結尾還是使用'\0'兼容原有的C語言規范)

2.2 Redis中的HashMap-dict

由于C語言中沒有實現字典類型的數據結構,所以Redis提供了它的實現,字典可以理解成Java中的HashMap,功能與之類似。當一個hash類型的key包含的鍵值對較多,或者鍵值對中的元素都是較長字符串時,底層就會使用字典來作為底層實現

127.0.0.1:6379> hset user:001 bigvalue ababababaabababaababbababaabbaababababababababababababababababababababbabaabababababababababaababababababaababababababaab
(integer) 1
127.0.0.1:6379> object encoding user:001
"hashtable"

dictht(dict.h)-哈希表

typedef struct dictht {// 為了實現漸進式的rehash
    dictEntry **table;// 存放key value的entry
    unsigned long size; // hashtable的容量
    unsigned long sizemask; // size - 1,用于取模運算相當于2^(size - 1)
    unsigned long used;// hashtable的元素個數
} dictht;

負載因子:use/size

Hash沖突及解決:與JDK的實現類似,剛開始申請的數組不夠用,需要進行擴容,就需要重新申請一塊內存區域,然后將數據搬移到新的內存區域上,在出現Hash沖突時,會將沖突的元素用鏈表鏈起來,這種方式叫“鏈地址法”

舉例說明,假設存儲的是<key1, value1>,<key2, value2>, <key3, value3>,默認情況下hashtable的初始大小為4,所以用長度為4的數組來存放,上圖橙色方塊為slot(哈希槽),hash(key2) %4 =2且hash(key3) %4 =2,都會放入槽位2中,這種情況就是Hash碰撞,在JDK的HashMap中在碰撞較少時會使用鏈表存放,此處也是類似的,采用鏈表將碰撞的key連起來,當碰撞到一定長度后會進行重新擴容,這里擴容比較粗暴直接*2即可,所以翻閱源碼會看到dict里會有兩個dictht(dict hash table的意思)的結構體,就是為了擴容做準備的。假設當前的槽位是4個,擴容時會向操作系統申請8個槽位的內存空間,再將另一個dictht的指針指向這塊區域,然后將原有的dictht逐漸遷移到新的dictht

注意:取模運算的優化:num % 2^n = num & (2^n - 1)

2.2.1 Hash碰撞問題

hash碰撞.png

為了讓鏈表不要太長,所以負載因子需要維持在一個合理的范圍內,

以下情況會進行hash表的擴展(即上述橙色格子加多一倍的數量,并對數據重新rehash放入新的數組中)
1)服務端沒有在執行BGSAVE或BGREWRITEAOF命令,并且負載因子大于等于1
2)服務器正在執行BGSAVE或BGREWRITEAOF命令,并且負載因子大于等于5

以下情況會進行hash表的收縮
1)負載因子小于0.1

2.2.2 漸進式rehash

這里還需要特別說明地是,為了避免rehash對服務性能造成影響,Redis服務端不是一次性將ht[0]的所有鍵值對全部rehash到ht[1],而是分多次、漸進式的進行遷移數據,每次對字典執行增刪改查時執行對應的key會順帶做遷移,當然在Redis空閑時也會有一個事件輪詢的機制進行數據遷移,順帶一提的是,在進行查找的時候,會對這兩個dictht的map都進行搜索,直到搬遷完畢,搬遷的過程實在主線程中執行的,用后臺線程就要考慮邊搬遷邊有數據寫入,由于是漸進式的所以搬遷數據速度也非常快,所以不會阻塞太長

2.3 Redis的5種對象底層剖析

在Redis中不管是Key還是Value都是用realObject來表示的

typedef struct redisObject {// string/list/set/hash/zset...
    unsigned type:4; // 通過type標識value的類型,這個type就可以限定不同的redisObject只能執行相對應的指令 通過type key進行查看 占4bit
    unsigned encoding:4; // 通過object encoding key進行查看,類型有:raw,embstr,int,ziplist... 占4bit
    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or 占3byte(24bit)
                            * LFU data (least significant 8 bits  內存淘汰策略相關frequency
                            * and most significant 16 bits access time). */ 
    int refcount; // 4byte 使用引用計數管理內存 
    void *ptr;    // 8byte 指向底層實現的數據結構的指針
} robj; // 共占16byte

2.3.1 string

Redis中的string,key是sds類型,而value類型也是sds類型(所以通過type查看都是string),但sds底層實現(通過命令encoding object [key])查看)則有int、raw、embstr,表示的是Redis底層的真實數據結構,在Redis中稱為encoding

redisObject對應ptr.png

1)int

試想一下存儲整型的數字,而redisObject中的指針就占了8byte,但是long類型在64bit OS中也是占用8byte,若存儲整型還要向內存開辟多8byte的空間,然后再用redisObject的ptr來指向是不是一種浪費,又要申請內存,還要CPU多操作去尋址找到對應存儲long類型的區域。

Redis基于以上的原因對可以用8byte存儲的長整型做了優化,直接使用ptr存放長整型,在計算機中64bit的空間能存放的長整形的范圍為(-263)~(263)*即-9,223,372,036,854,775,808~9,223,372,036,854,775,807

所以當字符串小于等于20byte時,Redis會嘗試轉成長整型來存儲,若可以,直接用ptr指針來存儲此長整型,這樣存儲能減少一次CPU IO也減少了另外開辟空間,時間空間上都能節省

# 存入64bit長整型的右邊界
127.0.0.1:6379:0>set strInt 9223372036854775807
"OK"
# 觀察到確實是int
127.0.0.1:6379:0>debug object strInt 
"Value at:00007FCD875B56A0 refcount:1 encoding:int serializedlength:20 lru:5249254 lru_seconds_idle:2"
# 存入64bit長整型的右邊界+1
127.0.0.1:6379:0>set strInt 9223372036854775808
"OK"
# 觀察到已經轉換成了embstr
127.0.0.1:6379:0>debug object strInt 
"Value at:00007FCD875B5640 refcount:1 encoding:embstr serializedlength:20 lru:5249266 lru_seconds_idle:2"
# 同理左邊界
127.0.0.1:6379:0>set strInt -9223372036854775808
"OK"
127.0.0.1:6379:0>object encoding strInt
"int"

2)embstr

redisObject結構體總共占16byte(type:4bit, encoding:4bit, lru:3byte, refcount:4byte, *ptr:8byte)

typedef struct redisObject {// string/list/set/hash/zset...
    unsigned type:4; // 通過type標識value的類型 占4bit
    unsigned encoding:4; // 通過object encoding key進行查看,類型有:raw,embstr,int,ziplist... 占4bit
    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or 占3byte(24bit)
                            * LFU data (least significant 8 bits  內存淘汰策略相關frequency
                            * and most significant 16 bits access time). */ 
    int refcount; // 4byte 使用引用計數管理內存 
    void *ptr;    // 8byte 指向sds
} robj; // 共占16byte

CPU通過內存地址獲取數據通過cpu cache line(一次取64byte)來取數據,由于取RedisObject的時候會獲取64byte,Redis將redisObject后的48byte利用起來,而sdshdr8占3byte,字符結束符'\0'占1byte,所以只剩下44byte能夠放數據

embstr存儲優化.png
# 44位的str
127.0.0.1:6379:0>set strInt 12345678901234567890123456789012345678901234
"OK"
127.0.0.1:6379:0>object encoding strInt
"embstr"
# 45位的str
127.0.0.1:6379:0>set strInt 123456789012345678901234567890123456789012345
"OK"
127.0.0.1:6379:0>object encoding strInt
"raw"

3)raw

當總體超過64byte,Redis認為是大字符串,則直接使用raw形式

string類型的應用:億級用戶日活統計BitMap實戰

假設當前有一個場景需要統計上億級別的用戶的上線情況,如近三天的上線用戶個數,或者是連續三天上線的用戶個數,可以使用BitMap進行實現,BitMap類型實際上是sds,底層存儲方式是以char[]實現的

bitmap場景-1.png

上圖表示用bitmap的offset作為用戶id,第2列表示用戶0上過線,第3列表示用戶1未上過線,第4列表示用戶2上過線

bitmap場景-2.png

上圖表示第2列表示20200520這天用戶0上線了,20200521這天用戶0也上線了,20200522這天用戶0未上線

# 設置用戶0在20200520當天有上線
127.0.0.1:6379> setbit login_20200520 0 1
(integer) 0
127.0.0.1:6379> setbit login_20200520 5 1
(integer) 0
# 0x84 -> 1000 0100
127.0.0.1:6379> get login_20200520
"\x84"
127.0.0.1:6379> setbit login_20200521 0 1
(integer) 0
127.0.0.1:6379> setbit login_20200521 2 1
(integer) 0
127.0.0.1:6379> setbit login_20200521 5 1
(integer) 0
127.0.0.1:6379> setbit login_20200522 3 1
(integer) 0
# 設置用戶5在20200522當天有上線
127.0.0.1:6379> setbit login_20200522 5 1
(integer) 0
# 由于是5bit,所以只用1byte就能存儲起來
127.0.0.1:6379> strlen login_20200520
(integer) 1
# 統計20200520的用戶上線人數
127.0.0.1:6379> bitcount login_20200520
(integer) 2
# 指定20200521,id從3-5的用戶上線人數
127.0.0.1:6379> bitcount login_20200521 3 5
(integer) 0

##連續三天都登錄的用戶個數
127.0.0.1:6379> bitop and login_20200520-20200522 login_20200520 login_20200521 login_20200522
(integer) 1
# 說明只有一個用戶連續三天都登錄了
127.0.0.1:6379> bitcount login_20200520-20200522
(integer) 1

##近三天有登錄過的用戶個數(bitop中的或運算)
127.0.0.1:6379> bitop or active_20200520-20200522 login_20200520 login_20200521 login_20200522
(integer) 1
127.0.0.1:6379> get active_20200520-20200522
"\xb4"
127.0.0.1:6379> get active_20200520-20200522
"\xb4"
127.0.0.1:6379> bitcount active_20200520-20200522
(integer) 4

說明:bitmap最多能存儲 2^32-1 bit(即512M),BitCount的計算速度快是由于用了“漢明重量”的算法

2.3.2 list

在翻閱源碼前,想自己思考一下,list會如何實現?

由于操作時有lpush rpush lpop rpop,一般人都會想著通過雙向鏈表實現。

使用雙向鏈表的實現會出現以下問題,

問題1:pre、next兩個指針,一個指針要占用8byte,兩個即16byte,但是可能存放的數據就小于8byte,這種情況就叫胖指針。意思是指針占據了絕大多數空間而不是被數據占據。

問題2:鏈表創建可能會產生大量的內存碎片

為了解決胖指針的問題,在Redis 3.2之前是使用ziplist和linkedlist來進行存儲的,壓縮列表相對于雙向鏈表更節省內存,所以在創建list時,會先考慮壓縮列表,并在一定條件下才轉化為雙向鏈表,在說明轉化條件之前,我們先了解一下什么是壓縮列表。

1)ziplist

ziplist&demo.png

壓縮列表是Redis為了節省內存而設計的,是由連續內存空間組成的順序型數據,每個entry可以是字節數組或整數值

ziplist節省內存的原因

因為節點的prerawlen屬性記錄了前一個節點的長度,所以程序可以通過指針運算,根據當前節點的初始地址來計算出前一個節點的起始地址。例如,假設當前指向entry2的指針為p_entry2,則 p_entry1=p_entry2-prerawlen,通過這樣的計算方式就能不保留前一個節點的指針從而節約了內存。

創建新列表時 redis 默認使用 redis_encoding_ziplist 編碼, 當以下任意一個條件被滿足時, 列表會被轉換成 redis_encoding_linkedlist 編碼:

  • 試圖往列表新添加一個字符串值,且這個字符串的長度超過 server.list_max_ziplist_value (默認值為 64 )。
  • ziplist 包含的節點超過 server.list_max_ziplist_entries (默認值為 512 )。

注意:這兩個條件是可以修改的,在 redis.conf 中:

list-max-ziplist-value 64 
list-max-ziplist-entries 512 

ziplist連鎖更新問題

因為在ziplist中,每個zlentry都存儲著前一個節點所占的字節數,而這個數值又是變長編碼的。假設存在一個壓縮列表,其包含e1、e2、e3、e4…..,e1節點的大小為253字節,那么e2.prevrawlen的大小為1字節,如果此時在e2與e1之間插入了一個新節點e_new,e_new編碼后的整體長度(包含e1的長度)為254字節,此時e2.prevrawlen就需要擴充為5字節;如果e2的整體長度變化又引起了e3.prevrawlen的存儲長度變化,那么e3也需要擴…….如此遞歸直到表尾節點或者某一個節點的prevrawlen本身長度可以容納前一個節點的變化。其中每一次擴充都需要進行空間再分配操作。刪除節點亦是如此,只要引起了操作節點之后的節點的prevrawlen的變化,都可能引起連鎖更新。

連鎖更新在最壞情況下需要進行N次空間再分配,而每次空間再分配的最壞時間復雜度為O(N),因此連鎖更新的總體時間復雜度是O(N^2)。
即使涉及連鎖更新的時間復雜度這么高,但它能引起的性能問題的概率是極低的:需要列表中存在大量的節點長度接近254個entry。

所以Redis3.2后為了解決這個問題有引入了quicklist

2)quicklist

可以認為quickList,是ziplist和linkedlist二者的結合;quickList將二者的優點結合起來。

quicklist數據結構.png

quickList就是一個標準的雙向鏈表的配置,有head 有tail;
每一個節點是一個quicklistNode,包含prev和next指針。
每一個quicklistNode 包含 一個ziplist,*zp 壓縮鏈表里存儲鍵值。
所以quicklist是對ziplist進行一次封裝,使用小塊的ziplist來既保證了少使用內存,也保證了性能。

# Lists are also encoded in a special way to save a lot of space.
# The number of entries allowed per internal list node can be specified
# as a fixed maximum size or a maximum number of elements.
# For a fixed maximum size, use -5 through -1, meaning:
# -5: max size: 64 Kb  <-- not recommended for normal workloads
# -4: max size: 32 Kb  <-- not recommended
# -3: max size: 16 Kb  <-- probably not recommended
# -2: max size: 8 Kb   <-- good
# -1: max size: 4 Kb   <-- good
# Positive numbers mean store up to _exactly_ that number of elements
# per list node.
# The highest performing option is usually -2 (8 Kb size) or -1 (4 Kb size),
# but if your use case is unique, adjust the settings as necessary.
# 設置單個ziplist節點最大存儲8kb,超過則進行分裂,將數據存儲到新的ziplist節點
list-max-ziplist-size -2
# 0代表所有節點,都不進行壓縮,1代表前面兩個和尾部兩個不進行壓縮,其它均壓縮,2,3,4...以此類推
list-compress-depth 1

2.3.3 hash

1)ziplist

Hash對象的編碼可以是ziplist或者hashtable,當數據量比較小,或單個元素較小時,底層用ziplist存儲。數據大小和元素數量閾值可以通過以下參數進行設置

# ziplist元素個數超過512,將改為hashtable編碼
hash-max-ziplist-entries 512
# 單個元素大小超過64byte,將改為hashtable編碼
hash-max-ziplist-value 64
## 測試hash的object encoding
127.0.0.1:6379> hmset user:001 name Vander age 18
OK
127.0.0.1:6379> type user:001
hash
127.0.0.1:6379> object encoding user:001
"ziplist"
#寫入超過64byte的value,會轉換成hashtable
127.0.0.1:6379> hset user:001 bigvalue ababababaabababaababbababaabbaababababababababababababababababababababbabaabababababababababaababababababaababababababaab
(integer) 1
127.0.0.1:6379> object encoding user:001
"hashtable"

當有新的鍵值對要加入到hash對象時,程序會先將保存了鍵的壓縮列表節點推入到壓縮列表尾部,然后再將保存了值的壓縮列表節點推入到壓縮列表尾部,如下圖所示,hset name Vander在ziplist中是這么存儲的:

hash-ziplist.png

使用這種方式保存時,并不需要申請多余的內存空間,而且每個Key都要存儲一些關聯的系統信息(如過期時間、LRU等),因此和String類型的Key/Value相比,Hash類型極大的減少了Key的數量(大部分的Key都以Hash字段的形式表示并存儲了),從而進一步優化了存儲空間的使用效率。

在這篇 redis memory optimization 官方文章中,作者強烈推薦使用hash存儲數據

hash vs string

若存放user對象,string會這么存儲,而hash會這么存儲

hash vs string.png

2)hashtable

hashtable 編碼的哈希對象使用字典作為底層實現, 哈希對象中的每個鍵值對都使用一個字典鍵值對來保存:

  • 字典的每個鍵都是一個字符串對象, 對象中保存了鍵值對的鍵;
  • 字典的每個值都是一個字符串對象, 對象中保存了鍵值對的值。
hash-hashtable.png

具體使用哪種數據結構,其實是需要看你要存儲的數據以及使用場景。

如果存儲的都是比較結構化的數據,比如用戶數據緩存,或者經常需要操作數據的一個或者幾個,特別是如果一個數據中如果filed比較多,但是每次只需要使用其中的一個或者少數的幾個,使用hash是一個好的選擇,因為它提供了hget 和 hmget,而無需取出所有數據再在代碼中處理。

反之,如果數據差異較大,操作時常常需要把所有數據都讀取出來再處理,使用string 是一個好的選擇。

還有一種場景:如果一個hash中有大量的field(成千上萬個),需要考慮是不是使用string來分開存儲是不是更好的選擇。

2.3.4 set

Set為無序的,自動去重的集合數據類型,Set的encoding可以是intset或者是value為null的hashtable,當數據可以用整型表示時,set集合將被編碼為intset數據結構

當滿足以下兩個條件時,set將用hashtable存儲數據:
1)元素個數大于set-max-intset-entries
2)元素無法用整型表示

# intset能存儲的最大元素個數,超過則用hashtable編碼
set-max-intset-entries 512
## 測試set類型的object encoding
127.0.0.1:6379> sadd group_nums 1 2 3 5 4 0
(integer) 6
# 當元素量少,且可用整型表示時,會用intset,并且排好序,方便查找
127.0.0.1:6379> smembers group_nums
1) "0"
2) "1"
3) "2"
4) "3"
5) "4"
6) "5"
127.0.0.1:6379> type group_nums
set
127.0.0.1:6379> object encoding group_nums
"intset"
# 當添加了非整型類型后,轉成hashtable
127.0.0.1:6379> sadd group_nums a
(integer) 1
127.0.0.1:6379> object encoding group_nums
"hashtable"

1)intset

整數集合是一個有序的,存儲整型數據的結構。整型集合在Redis中可以保存int16_t,int32_t,int64_t類型的整型數據,并且可以保證集合中不會出現重復數據

typedef struct intset {
    uint32_t  encoding;// 編碼類型 可以是INTSET_ENC_INT16,INTSET_ENC_INT32,INTSET_ENC_INT64
    uint32_t  length;  // 元素個數
    int8_t    contents[]; // 存儲元素的數組,可以是int16_t,int32_t,int64_t
} intset; 
#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))

升級

每當將新元素添加到集合,并且新元素的類型比整數集合現有所有元素的類型都要長時,整數集合需要進行升級,然后才能將新元素添加到整數集合中,并且升級之后不會進行降級

分三步進行:
1)根據新元素的類型,擴展整數集合底層數組的空間大小并為新元素分配空間
2)將底層數組現有的所有元素都轉換成新元素相同的類型,并將類型轉換后的元素放在正確的位置(即要排好序放)
3)將新元素放入

2)hashtable

set-hashtable.png

2.3.5 zset

有序集合的encoding可以是ziplist或者skiplist

1)ziplist

壓縮列表內的集合元素按score從小到大進行排序,分值較小的元素被放置在靠近表頭的位置,分值較大的則放置在靠近表尾的位置

以下是“zadd accomplishment 100 math",表示存儲數學成績100分

zset-ziplist.png

2)dict+skiplist

encoding為skiplist的zset對象使用zset結構體作為底層實現,一個zset的結構體同時有字典和跳表

typedef struct zset {
    dict *dict;
    zskiplist *zsl;
} zset;

先簡單展示一下,跳表查找72的過程

跳表的簡單演示.png

紅色箭頭為查找路徑,通過索引層可以實現類似于二分查找的結果,所以跳表的查找時間復雜度為log(n)

Redis中的跳表誰能成為索引,是由隨機函數隨機決定的,越底層的元素能成為索引的可能性越大

下面展示"zadd accomplishment 100 math 99 chinese 95 English"

zset-skiplist.png

從上圖可以看出skiplist主要是用于通過score來給內容進行排序,若只是需要單獨取出某個內容的score,可以直接查詢dict結構,這種方式使用了空間換取了時間,但是占得并不多,以上重復展示了個元素的成員和分值,其實字典和跳表是會共享元素的成員和分值,不會造成數據重復存儲的。(注意上述只是為了舉例,實際上上述指令encoding并不是skiplist,因為元素數量和元素長度都不滿足條件,所以上述圖示實際上是用ziplist存儲的)

同時滿足以下兩個條件時,對象使用ziplist編碼:
1)有序集合保存的元素數量小于128個
2)有序集合保存的所有元素成員的長度都小于64byte

127.0.0.1:6379> zadd accomplishment 100 math 99 chinese 95 English
(integer) 3
127.0.0.1:6379> object encoding accomplishment
"ziplist"
127.0.0.1:6379> zadd accomplishment 50 abcdaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
(integer) 1
127.0.0.1:6379> object encoding accomplishment
"skiplist"
127.0.0.1:6379>

注意:GEO類型是GeoHash算法+zset來實現的。

擴展知識

Redis 3.2前后sds的結構

Redis 3.2后sds的定義(sds.h)

struct sdshdr {
    int len;// 字符串的長度,不會計算'\0'
    int free;
    char buf[];
}

Redis 3.2后sts的定義(sds.h)

typedef char *sds;

/* Note: sdshdr5 is never used, we just access the flags byte directly.
 * However is here to document the layout of type 5 SDS strings. */
struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* used */
    uint8_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];// 末尾的結束符'\0'會占用一個字節
};// 所以這個結構占用了4byte
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* 2byte used */
    uint16_t alloc; /* 2byte excluding the header and null terminator */
    unsigned char flags; /* 1byte 3 lsb of type, 5 unused bits */
    char buf[];// 1byte
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; /* used */
    uint32_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* 已用長度 used */
    uint64_t alloc; /* 申請內存空間大小 excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[]; /* 真正存放數據的地方 char類型在C語言中占用1byte */ 
};

可以看出Redis的sts類型存放字符串也是通過char數組,其中還有len標記這個字符串的長度,還有alloc表示申請的總空間的大小

Redis 3.2之后對底層的sts類型進行了優化,當len小于5bit時,即存放的數據范圍為0~(25-1)時,直接使用sdshdr5進行存儲即可。當存放的數據范圍超過25-1,后面的5bit就會閑置無用。

sdshdr5-flags示意圖.png

在64位系統c語言int占用4byte(32 bit),能表示20位字符的長度(由于232次方是42億),則使用sdshdr5,若數組長度為232~2^64,則使用sdshdr64,以此類推。

Redis的數據結構關聯關系

redis的數據結構.png

Redis中db類型:redisDb(server.h)

typedef struct redisDb {
    dict *dict;                 /* 存放<k, value> The keyspace for this DB */
    dict *expires;              /* 存放<key timeout>Timeout of keys with a timeout set */
    dict *blocking_keys;        /* 阻塞隊列的處理 Keys with clients waiting for data (BLPOP)*/
    dict *ready_keys;           /* 客戶端維護 Blocked keys that received a PUSH */
    dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */
    int id;                     /* 數據庫id Database ID */
    long long avg_ttl;          /* Average TTL, just for stats */
    unsigned long expires_cursor; /* Cursor of the active expire cycle. */
    list *defrag_later;         /* List of key names to attempt to defrag one by one, gradually. */
} redisDb;

dict(dict.h)

typedef struct dict {
    dictType *type; // 類型
    void *privdata; //
    dictht ht[2];// 需要有兩個dictht 為了實現漸進式的rehash,在沒有進行rehash的階段h[1]=null
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    int16_t pauserehash; /* If >0 rehashing is paused (<0 indicates coding error) */
} dict;

dictht(dict.h)-哈希表

typedef struct dictht {// 為了實現漸進式的rehash
    dictEntry **table;// 存放key value的entry
    unsigned long size; // hashtable的容量
    unsigned long sizemask; // size - 1,用于取模運算相當于2^(size - 1)
    unsigned long used;// hashtable的元素個數
} dictht;

dictEntry(dict.h)

typedef struct dictEntry {
    void *key; // 實際上就是sds的結構體,由于key都是用string存儲的
    union {
        void *val;// value的類型則比較多樣,string/list/hash等,對應的結構體是redisObject
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next; // 當hash沖突時,指向下個entry
} dictEntry;

dictEntry中的value是通過redisObject來進行存放的(server.h)

typedef struct redisObject {// string/list/set/hash/zset...
    unsigned type:4; // 通過type標識value的類型,這個type就可以限定不同的redisObject只能執行相對應的指令 通過type key進行查看 占4bit
    unsigned encoding:4; // 通過object encoding key進行查看,類型有:raw,embstr,int,ziplist... 占4bit
    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or 占3byte(24bit)
                            * LFU data (least significant 8 bits  內存淘汰策略相關frequency
                            * and most significant 16 bits access time). */ 
    int refcount; // 4byte 使用引用計數管理內存 
    void *ptr;    // 8byte 指向sds
} robj; // 共占16byte

set key value源碼解析

源碼說明當redisObject的字節長度小于等于20時,會嘗試使用長整型進行存儲

setCommand(t_string.c)

void setCommand(client *c) {
    robj *expire = NULL;
    int unit = UNIT_SECONDS;
    int flags = OBJ_NO_FLAGS;

    if (parseExtendedStringArgumentsOrReply(c,&flags,&unit,&expire,COMMAND_SET) != C_OK) {
        return;
    }

    c->argv[2] = tryObjectEncoding(c->argv[2]);// 完成編碼
    setGenericCommand(c,flags,c->argv[1],c->argv[2],expire,unit,NULL,NULL);
}

tryObjectEncoding(object.c)

robj *tryObjectEncoding(robj *o) {
    long value;
    sds s = o->ptr;
    size_t len;

    /* Make sure this is a string object, the only type we encode
     * in this function. Other types use encoded memory efficient
     * representations but are handled by the commands implementing
     * the type. */
    serverAssertWithInfo(NULL,o,o->type == OBJ_STRING);

    /* We try some specialized encoding only for objects that are
     * RAW or EMBSTR encoded, in other words objects that are still
     * in represented by an actually array of chars. */
    if (!sdsEncodedObject(o)) return o;

    /* It's not safe to encode shared objects: shared objects can be shared
     * everywhere in the "object space" of Redis and may end in places where
     * they are not handled. We handle them only as values in the keyspace. */
     if (o->refcount > 1) return o;

    /* Check if we can represent this string as a long integer.
     * Note that we are sure that a string larger than 20 chars is not
     * representable as a 32 nor 64 bit integer. */
    len = sdslen(s);// 獲取字符串的字節長度
    if (len <= 20 && string2l(s,len,&value)) {// 字符串長度小于等于20byte 嘗試轉成長整形存儲
        /* This object is encodable as a long. Try to use a shared object.
         * Note that we avoid using shared integers when maxmemory is used
         * because every object needs to have a private LRU field for the LRU
         * algorithm to work well. */
        if ((server.maxmemory == 0 ||
            !(server.maxmemory_policy & MAXMEMORY_FLAG_NO_SHARED_INTEGERS)) &&
            value >= 0 &&
            value < OBJ_SHARED_INTEGERS)
        {
            decrRefCount(o);
            incrRefCount(shared.integers[value]);
            return shared.integers[value];
        } else {
            if (o->encoding == OBJ_ENCODING_RAW) {
                sdsfree(o->ptr);
                o->encoding = OBJ_ENCODING_INT;// encoding為int
                o->ptr = (void*) value;// 使用長整型存儲
                return o;
            } else if (o->encoding == OBJ_ENCODING_EMBSTR) {
                decrRefCount(o);
                return createStringObjectFromLongLongForValue(value);
            }
        }
    }

    /* If the string is small and is still RAW encoded,
     * try the EMBSTR encoding which is more efficient.
     * In this representation the object and the SDS string are allocated
     * in the same chunk of memory to save space and cache misses. */
    if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT) {
        robj *emb;

        if (o->encoding == OBJ_ENCODING_EMBSTR) return o;
        emb = createEmbeddedStringObject(s,sdslen(s));
        decrRefCount(o);
        return emb;
    }

    /* We can't encode the object...
     *
     * Do the last try, and at least optimize the SDS string inside
     * the string object to require little space, in case there
     * is more than 10% of free space at the end of the SDS string.
     *
     * We do that only for relatively large strings as this branch
     * is only entered if the length of the string is greater than
     * OBJ_ENCODING_EMBSTR_SIZE_LIMIT. */
    trimStringObjectIfNeeded(o);

    /* Return the original object. */
    return o;
}

Redis 6.0新特性

1)多線程
2)服務端協助的客戶端緩存(僅支持單機,lettuce 6.0版本已經支持)
3)權限管理(進行更加細致的權限管理)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,488評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,034評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,327評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,554評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,337評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,883評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,975評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,114評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,625評論 1 332
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,555評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,737評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,244評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,973評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,615評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,343評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,699評論 2 370

推薦閱讀更多精彩內容