概要
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字符串不記錄長度,所以每次增減字符都要對保存的C字符串的數組進行內存重分配。
Redis為了解決增加字符數,頻繁重分配的問題,會進行內存預分配,來減少由于再次增加字符而導致的內存重分配的問題。即,假設原有sds存放char[] = 'V','a','n','d','e','r','\0',需要存放"Vander&Jason",由于此字符串占12byte,所以會預分配25byte(12*2+1,'\0'占1byte)。但是在大于1MB之后,每次新增不會按照兩倍擴展了,只能會再分配多1MB。
為了解決縮少字符數,導致的內存泄漏的問題,通過調整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表的擴展(即上述橙色格子加多一倍的數量,并對數據重新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
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能夠放數據
# 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的offset作為用戶id,第2列表示用戶0上過線,第3列表示用戶1未上過線,第4列表示用戶2上過線
上圖表示第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
壓縮列表是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就是一個標準的雙向鏈表的配置,有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中是這么存儲的:
使用這種方式保存時,并不需要申請多余的內存空間,而且每個Key都要存儲一些關聯的系統信息(如過期時間、LRU等),因此和String類型的Key/Value相比,Hash類型極大的減少了Key的數量(大部分的Key都以Hash字段的形式表示并存儲了),從而進一步優化了存儲空間的使用效率。
在這篇 redis memory optimization 官方文章中,作者強烈推薦使用hash存儲數據
hash vs string
若存放user對象,string會這么存儲,而hash會這么存儲
2)hashtable
hashtable 編碼的哈希對象使用字典作為底層實現, 哈希對象中的每個鍵值對都使用一個字典鍵值對來保存:
- 字典的每個鍵都是一個字符串對象, 對象中保存了鍵值對的鍵;
- 字典的每個值都是一個字符串對象, 對象中保存了鍵值對的值。
具體使用哪種數據結構,其實是需要看你要存儲的數據以及使用場景。
如果存儲的都是比較結構化的數據,比如用戶數據緩存,或者經常需要操作數據的一個或者幾個,特別是如果一個數據中如果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
2.3.5 zset
有序集合的encoding可以是ziplist或者skiplist
1)ziplist
壓縮列表內的集合元素按score從小到大進行排序,分值較小的元素被放置在靠近表頭的位置,分值較大的則放置在靠近表尾的位置
以下是“zadd accomplishment 100 math",表示存儲數學成績100分
2)dict+skiplist
encoding為skiplist的zset對象使用zset結構體作為底層實現,一個zset的結構體同時有字典和跳表
typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset;
先簡單展示一下,跳表查找72的過程
紅色箭頭為查找路徑,通過索引層可以實現類似于二分查找的結果,所以跳表的查找時間復雜度為log(n)
Redis中的跳表誰能成為索引,是由隨機函數隨機決定的,越底層的元素能成為索引的可能性越大
下面展示"zadd accomplishment 100 math 99 chinese 95 English"
從上圖可以看出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就會閑置無用。
在64位系統c語言int占用4byte(32 bit),能表示20位字符的長度(由于232次方是42億),則使用sdshdr5,若數組長度為232~2^64,則使用sdshdr64,以此類推。
Redis的數據結構關聯關系
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)權限管理(進行更加細致的權限管理)