Redis 是一個鍵值對數據庫(key-value DB),數據庫的值可以是字符串、集合、列表等多種類型的對象,而數據庫的鍵則總是字符串對象。
以下內容摘自(但不僅限于,其他地方引用有相應標注):
Redis 命令參考
Redis 設計與實現(第一版)
Redis 設計與實現(第二版)
Redis 設計與實現
Redis內部數據結構詳解之跳躍表(skiplist)
Redis內部數據結構詳解之字典(dict)
Redis中5種數據結構的使用場景介紹
前言
談文章里的一些叫法?
- 宏觀上:我們看到的是一些基本數據類型,如我們常用的哈希,列表,集合等;微觀上:這此數據都是由Redis的底層(或稱內部)數據結構來支撐的,比如字典,跳躍表;
- 我們管這種宏觀的基本數據類型,比如哈希叫哈希(類型)鍵,我們管有序集合叫有序集合鍵;
- Redis 的每一種數據類型,比如字符串、列表、有序集,它們都擁有不只一種底層實現(Redis 內部稱之為編碼,encoding)
關于文章想帶來什么?
- 宏觀的基本數據類型VS底層數據結構的關系如何;
- 底層數據結構長什么樣子(整數集合、壓縮列表進一步劃分,屬于內存映射數據結構,本文不作詳細描述,后續可以專門分析下它們是如何相比其他數據結構節約內存的);
- 更多地,燃起對數據結構的一些興趣;
Redis六種內部數據結構
- Sds (sds)
- 雙端鏈表(linkedlist)
- 字典(dict)
- 跳躍表(skiplist)
- 整數集合(intset)
- 壓縮列表(ziplist)
1. Sds
Sds (Simple Dynamic String,簡單動態字符串)是 Redis 底層所使用的字符串表示,幾乎所有的 Redis 模塊中都用了 sds。Sds 在 Redis 中的主要作用有以下兩個:
- 實現字符串對象(StringObject);http://redisbook.com/preview/sds/content.html
- 在 Redis 程序內部用作 char* 類型的替代品;
主要特點:
Redis的簡單動態字符串SDS對比C語言的字符串char*,有以下特性:
- 可以在O(1)的時間復雜度得到字符串的長度
- 可以高效的執行append追加字符串操作
- 二進制安全
應用場景:
- 其他模塊(幾乎每一個)都用到了 sds 類型值:用來保存數據庫中的字符串值
- SDS 還被用作緩沖區(buffer): 客戶端傳入服務器的協議內容,AOF 模塊中的 AOF 緩沖區, 以及客戶端狀態中的輸入緩沖區, 都是由 SDS 實現的
數據結構:
其中,類型 sds 是 char * 的別名(alias),而結構 sdshdr 則保存了 len 、 free 和 buf 三個屬性
typedef char *sds;
struct sdshdr {
// buf 已占用長度
int len;
// buf 剩余可用長度
int free;
// 實際保存字符串數據的地方
char buf[];
};
為了易于理解,我們用一個 Redis 執行實例作為例子,解釋一下,當執行以下代碼時, Redis 內部發生了什么:
redis> SET msg "hello world"
OK
redis> APPEND msg " again!"
(integer) 18
redis> GET msg
"hello world again!"
1.鍵值對的鍵和值都以SDS對象保存:
鍵值對的鍵是一個字符串對象, 對象的底層實現是一個保存著字符串 "msg" 的 SDS 。
鍵值對的值也是一個字符串對象, 對象的底層實現是一個保存著字符串 "hello world" 的 SDS 。
- SET 命令創建并保存 hello world 到一個 sdshdr 中,這個 sdshdr 的值如下:
struct sdshdr {
len = 11;
free = 0;
buf = "hello world\0";
}
3.當執行 APPEND 命令時,相應的
sdshdr
被更新,字符串" again!"
會被追加到原來的"hello world"
之后,同時Redis 為 buf 創建了多于所需空間一倍的大小:
struct sdshdr {
len = 18;
free = 18;
buf = "hello world again!\0 "; // 空白的地方為預分配空間,共 18 + 18 + 1 個字節
}
2. 雙端鏈表
鏈表作為數組之外的一種常用序列抽象,是大多數高級語言的基本數據類型,因為 C 語言本身不支持鏈表類型,大部分 C 程序都會自己實現一種鏈表類型,Redis 也不例外 —— 實現了一個雙端鏈表結構。
主要特點:
- 節點帶有前驅和后繼指針,訪問前驅節點和后繼節點的復雜度為 (O(1)) ,并且對鏈表的迭代可以在從表頭到表尾和從表尾到表頭兩個方向進行;
- 鏈表帶有指向表頭和表尾的指針,因此對表頭和表尾進行處理的復雜度為 (O(1)) ;
- 鏈表帶有記錄節點數量的屬性,所以可以在 (O(1)) 復雜度內返回鏈表的節點數量(長度);
應用場景:
除了實現列表類型以外,雙端鏈表還被很多 Redis 內部模塊所應用
- 事務模塊使用雙端鏈表依序保存輸入的命令;
- 服務器模塊使用雙端鏈表來保存多個客戶端;
- 訂閱/發送模塊使用雙端鏈表來保存訂閱模式的多個客戶端;
- 事件模塊使用雙端鏈表來保存時間事件(time event);
數據結構:
雙端鏈表的實現由
listNode
和list
兩個數據結構構成:
其中, listNode
是雙端鏈表的節點:
typedef struct listNode {
// 前驅節點
struct listNode *prev;
// 后繼節點
struct listNode *next;
// 值
void *value;
} listNode;
而 list
則是雙端鏈表本身:
typedef struct list {
// 表頭指針
listNode *head;
// 表尾指針
listNode *tail;
// 節點數量
unsigned long len;
// 復制函數
void *(*dup)(void *ptr);
// 釋放函數
void (*free)(void *ptr);
// 比對函數
int (*match)(void *ptr, void *key);
} list;
注意, listNode
的 value
屬性的類型是 void *
,說明這個雙端鏈表對節點所保存的值的類型不做限制。
Redis 列表使用兩種數據結構作為底層實現:
1.雙端鏈表
2.壓縮列表
因為雙端鏈表占用的內存比壓縮列表要多,所以當創建新的列表鍵時,列表會優先考慮使用壓縮列表作為底層實現,并且在有需要的時候,才從壓縮列表實現轉換到雙端鏈表實現。
3. 字典
Redis 的字典使用哈希表作為底層實現(雙哈希表), 一個哈希表里面可以有多個哈希表節點, 而每個哈希表節點就保存了字典中的一個鍵值對。
主要特點:
Redis的字典是使用一個桶bucket,通過對key進行hash得到的索引值index,然后將key-value的數據存在桶的index位置,Redis處理hash碰撞的方式是鏈表,兩個不同的key hash得到相同的索引值,那么就使用鏈表解決沖突。使用鏈表自然當存儲的數據巨大的時候,字典不免會退化成多個鏈表,效率大大降低,Redis采用rehash的方式對桶進行擴容來解決這種退化。
小結:
- Redis中的字典數據結構使用哈希表來實現,用來存儲key-value鍵值元素;
- 字典使用兩個哈希表,一般只使用ht[0],只有當Rehash時候才使用ht[1];
- Redis 使用 MurmurHash2 算法來計算鍵的哈希值;
- 哈希表采用鏈表的方式解決鍵碰撞問題;
- 在對哈希表進行擴展或者收縮操作時, 程序需要將現有哈希表包含的所有鍵值對 rehash 到新哈希表里面, 并且這個 rehash 過程并不是一次性地完成的, 而是漸進式地完成的。
應用場景:
字典在 Redis 中的應用廣泛,使用頻率可以說和 SDS 以及雙端鏈表不相上下,基本上各個功能模塊都有用到字典的地方。
其中,字典的主要用途有以下兩個:
1.實現數據庫鍵空間(key space);
2.用作 Hash 類型鍵的底層實現之一;
- 實現數據庫鍵空間
Redis 是一個鍵值對數據庫,數據庫中的鍵值對由字典保存:每個數據庫都有一個對應的字典,這個字典被稱之為鍵空間(key space)。
redis> SET msg "hello world"
OK
在數據庫中創建一個鍵為 "msg" , 值為 "hello world" 的鍵值對時, 這個鍵值對就是保存在代表數據庫的字典里面的。
- 用作 Hash 類型鍵的底層實現之一
Redis 的 Hash 類型鍵使用以下兩種數據結構作為底層實現:
- 字典;
- 壓縮列表;
當一個哈希鍵包含的鍵值對比較多, 又或者鍵值對中的元素都是比較長的字符串時, Redis 就會使用字典作為哈希鍵的底層實現。
舉個例子, website 是一個包含 10086 個鍵值對的哈希鍵, 這個哈希鍵的鍵都是一些數據庫的名字, 而鍵的值就是數據庫的主頁網址:
redis> HSET website Redis "www.g.cn"
(integer) 1
redis> HSET website Redis.io "www.g.cn"
(integer) 1
redis> HSET website MariaDB "www.g.cn"
(integer) 1
...
redis> HLEN website
(integer) 10086
redis> HGETALL website
1) "Redis"
2) "Redis.io"
3) "MariaDB"
4) "MariaDB.org"
5) "MongoDB"
6) "MongoDB.org"
# ...
website 鍵的底層實現就是一個字典, 字典中包含了 10086 個鍵值對:
其中一個鍵值對的鍵為 "Redis" , 值為 "Redis.io" 。
另一個鍵值對的鍵為 "MariaDB" , 值為 "MariaDB.org" ;
還有一個鍵值對的鍵為 "MongoDB" , 值為 "MongoDB.org" ;
數據結構:
字典的數據結構和實現比較復雜,這里就針對一些比較有意思的特性直接得出結論:
Rehash的觸發機制:dictAdd 在每次向字典添加新鍵值對之前,都會對工作哈希表ht[0]進行檢查,如果used(哈希表中元素的數目)與size(桶的大小)比率ratio滿足以下任一條件,將激活字典的Rehash機制:
- 自然 rehash : ratio >= 1 ,且變量 dict_can_resize 為真。
- 強制 rehash : ratio 大于變量 dict_force_resize_ratio (目前版本中, dict_force_resize_ratio 的值為 5 )。
什么時候 dict_can_resize 會為假?
在前面介紹字典的應用時也說到過,數據庫就是字典,數據庫里的哈希類型鍵也是字典,當 Redis 使用子進程對數據庫執行后臺持久化任務時(比如執行
BGSAVE
或BGREWRITEAOF
時),為了最大化地利用系統的 copy on write 機制,程序會暫時將dict_can_resize
設為假,避免執行自然 rehash ,從而減少程序對內存的觸碰(touch)。
當持久化任務完成之后,dict_can_resize
會重新被設為真。
另一方面,當字典滿足了強制 rehash 的條件時,即使dict_can_resize
不為真(有BGSAVE
或BGREWRITEAOF
正在執行),這個字典一樣會被 rehash 。
Rehash執行過程:
- 1.創建一個比ht[0].used至少兩倍的ht[1].table;2.將原ht[0].table中所有元素遷移到ht[1].table;3.清空原來ht[0],將ht[1]替換成ht[0]。
- 漸進式Rehash主要由兩個函數來進行:
_dictRehashStep:當對字典進行添加、查找、刪除、隨機獲取元素都會執行一次(被動 rehash),其每次在開始Rehash后,將ht[0].table的第一個不為空的索引上的所有節點全部遷移到ht[1].table;
dictRehashMilliseconds:由 Redis 服務器常規任務程序(server cron job)執行,用于對數據庫字典進行主動 rehash,以毫秒為單位,在一定時間內,以每次執行100步rehash操作。
上面關于 rehash 的章節描述了通過 rehash 對字典進行擴展(expand)的情況,如果哈希表的可用節點數比已用節點數大很多的話,那么也可以通過對哈希表進行 rehash 來收縮(shrink)字典。
收縮 rehash 和上面展示的擴展 rehash 的操作幾乎一樣,只是它創建了一個比 ht[0]->table 小的 ht[1]->table。
4. 跳躍表
跳躍表(skiplist )是一種隨機化的數據,由 William Pugh 在論文《Skip lists: a probabilistic alternative to balanced trees》 中提出。事實上,這是早在1987年就誕生的東西。
主要特點:
- 跳躍表是一種隨機化數據結構,查找、添加、刪除操作都可以在對數期望時間(O(logn))下完成,效率可以比擬平衡二叉樹。
- 跳躍表目前在 Redis 的唯一作用,就是作為有序集類型的底層數據結構(之一,另一個構成有序集的結構是字典)。
- 它的設計巧妙和隨機化的根源來自于:當插入每個節點時需要決定它所要占據的層數,而這個層數正是通過一個算法返回的隨機層數值(Redis用ZSKIPLIST_MAXLEVEL來限制最高層數為32);特別的,當概率因子ZSKIPLIST_P(性能最優的取值是0.5,或0.25)為0.5時,正好形同拋硬幣的方式,即是:只要是正面就累加,直到遇見反面才停止,最后記錄正面的次數就是這里說的隨機層數;
參考:
https://blog.csdn.net/men_wen/article/details/70040026
https://www.cnblogs.com/flyfy1/archive/2011/02/24/1963347.html(有個形象的比喻)- 空間復雜度為 2n = O(n);跳表的高度期待為 Eh = O(log n),跳躍表就是一個在空間復雜度為2n的基礎上實現了查詢等操作在O(logn)的時間復雜度完成的一個性能優秀的數據結構。
參考:http://blog.sina.com.cn/s/blog_68f6d5370102uykh.html- 有人將跳躍表分析為鏈表+二分查找,可以借鑒(因為本身鏈表是沒有二分查找能力的——根本就在于沒有索引訪問能力,跳躍表利用多層鏈表的方式實現了類似索引的能力,就是多層,并且它是第三點說的隨機的多層;這樣很形象地將上層鏈表可以看成下層鏈表的索引,這樣就可以快速得跳過很多節點進行比較;是一種空間來換取時間的做法)。網上有一個阿里的面試問題是這樣問的:如何讓鏈表的元素查詢接近線性時間,其實就是指的跳躍表。
- 還有一種說法(未經證明):如果單純比較性能,跳躍表和紅黑樹可以說相差不大,但是加上并發的環境就不一樣了,如果要更新數據,跳躍表需要更新的部分就比較少,鎖的東西也就比較少,所以不同線程爭鎖的代價就相對少了,而紅黑樹有個平衡的過程,牽涉到大量的節點,爭鎖的代價也就相對較高了。性能也就不如前者了。不過這些對redis這個單進程單線程server來說都是浮云。
看了很多資料之后,突然覺得還是有個疑問,就是,為什么叫跳躍表(我感覺我還是沒有懂徹底)?
跳躍表(skiplist)是一種有序數據結構, 它通過在每個節點中維持多個指向其他節點的指針, 從而達到快速訪問節點的目的。跳躍表支持平均 O(\log N) 最壞 O(N) 復雜度的節點查找, 還可以通過順序性操作來批量處理節點。
通過一些話我們來領會下跳躍的含義:
- “就好像火車,有快車有慢車;快車停得站少,慢車停得多。所以,從一個地方到另一個地方,我們需要先乘坐快車,之后換乘慢車。”
- “底層是一個普通的有序鏈表。每個更高層都充當下面列表的「快速跑道」”;參考:https://blog.csdn.net/daniel_ustc/article/details/20218489
- 讓算法的效率“跳起來”!參考:https://www.cnblogs.com/flyfy1/archive/2011/02/24/1963347.html
(里面貼的幾個鏈接比較值得一看)
關于時間空間度?(目前我查閱了很多資料,始終未能找到有說服性的證明其時間復雜度為O(log n)的有效證明,好多都是生拉硬拽,有興趣的可以一起討論)
- (總體分析法):注意比較次數,在每一條鏈表上我們可以發現最多比較兩次,至少比較一次,所以比較次數不會超過 2LogN。所以比較的時間復雜度是 O(logN);(https://www.xuebuyuan.com/2115228.html)
- (想象比較法):可以把它相像成鏈表的二分查找:總共有n個元素,漸漸跟下去就是n,n/2,n/4,....n/2^k(接下來操作元素的剩余個數),其中k就是循環的次數;最后剩余為1說明找到, 于是n/2^k取整后>=1,得出 k=log2n,需要k次(即log2n),即得復雜度;有人也把跳躍表比作一顆二叉樹,可參考:http://courses.csail.mit.edu/6.046/spring04/handouts/skiplists.pdf
- (函數推演法)線段跳表 —— 跳表的一個拓展(https://wenku.baidu.com/view/7285945f804d2b160b4ec0a8.html)
- 原論文:http://www.cl.cam.ac.uk/teaching/0506/Algorithms/skiplists.pdf
查詢時間復雜度O(logn)有多快?效率是極其高的
上面說了跳表的高度期待O(log n),而Redis的跳躍表設定的高度限制是32層,可以反推出最理想最大的節點數量(即zskiplist最大的length)是232個。那么查詢一個元素所需要花的時間就是log2 232,即只需要32次循環,相當驚人(對比O(n)的復雜度體會一下)。因為232是一個相當大的數字,即為4 294 967 296,如,2的32次方ms是多少天:49.41天。
應用場景:
和字典、鏈表或者字符串這幾種在 Redis 中大量使用的數據結構不同,跳躍表在 Redis 的唯一作用,就是實現有序集數據類型。跳躍表將指向有序集的 score
值和 member
域的指針作為元素,并以 score
值為索引,對有序集元素進行排序。
參考:
https://www.kancloud.cn/kancloud/redisbook-first/63781
Lucene的跳躍表應用
數據結構:
https://blog.csdn.net/u014427196/article/details/52454462/ (可以通過圖直觀感受它的實現思路)
跳躍表是一種隨機化數據結構,基于并聯的鏈表(簡單的說,就是一個多層鏈表,你可以把每層看成一個鏈表),其效率可以比擬平衡二叉樹,查找、刪除、插入等操作都可以在對數期望時間內完成,對比平衡樹,跳躍表的實現要簡單直觀很多。
以下是個典型的跳躍表例子(圖片來自維基百科):
從圖中可以看出跳躍表主要有以下幾個部分構成:
1、 表頭head:負責維護跳躍表的節點指針
2、 節點node:實際保存元素值,每個節點有一層或多層
3、 層level:保存著指向該層下一個節點的指針
4、 表尾tail:全部由null組成
跳躍表的遍歷總是從高層開始,然后隨著元素值范圍的縮小,慢慢降低到低層。
Redis作者為了適合自己功能的需要,對原來的跳躍表進行了一下修改:
1、 允許重復的score值:多個不同的元素(member)的score值可以相同,若score值相同時,需要對比member,按字典排序存儲在跳表結構中。
2、 span存在于forward中,這個跨度字段的出現有助于快速計算元素在整個集合中的排名
3、 每個節點都有一個高度為1層的前驅指針forward,用于從底層表尾向表頭方向遍歷:當執行 ZREVRANGE 或 ZREVRANGEBYSCORE這類以逆序處理有序集的命令時,就會用到這個屬性。
4、 dict維護了skiplist的元素值(key)和分數(value)用于快讀的查找元素對應的分值以及判斷元素是否存在。
跳躍表數據結構如下:
//跳躍表節點
typedef struct zskiplistNode {
// member 對象
robj *obj;
// 分值
double score;
// 后退指針
struct zskiplistNode *backward;
// 層
struct zskiplistLevel {
// 前進指針
struct zskiplistNode *forward;
// 這個層跨越的節點數量
unsigned int span;
} level[];
} zskiplistNode;
//跳躍表
typedef struct zskiplist {
// 頭節點,尾節點
struct zskiplistNode *header, *tail;
// 節點數量
unsigned long length;
// 目前表內節點的最大層數
int level;
} zskiplist;
//有序集合
typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset;
Redis 使用跳躍表作為有序集合鍵的底層實現之一: 如果一個有序集合包含的元素數量比較多, 又或者有序集合中元素的成員(member)是比較長的字符串時, Redis 就會使用跳躍表來作為有序集合鍵的底層實現。
舉個例子, fruit-price 是一個有序集合鍵, 這個有序集合以水果名為成員, 水果價錢為分值, 保存了 130 款水果的價錢:
redis> ZRANGE fruit-price 0 2 WITHSCORES
1) "banana"
2) "5"
3) "cherry"
4) "6.5"
5) "apple"
6) "8"
redis> ZCARD fruit-price
(integer) 130
fruit-price 有序集合的所有數據都保存在一個跳躍表里面, 其中每個跳躍表節點(node)都保存了一款水果的價錢信息, 所有水果按價錢的高低從低到高在跳躍表里面排序:
跳躍表的第一個元素的成員為 "banana" , 它的分值為 5 ;
跳躍表的第二個元素的成員為 "cherry" , 它的分值為 6.5 ;
跳躍表的第三個元素的成員為 "apple" , 它的分值為 8 ;
內存映射數據結構
雖然內部數據結構非常強大,但是創建一系列完整的數據結構本身也是一件相當耗費內存的工作,當一個對象包含的元素數量并不多,或者元素本身的體積并不大時,使用代價高昂的內部數據結構并不是最好的辦法。為了解決這一問題,Redis在條件允許的情況下,會使用內存映射數據結構來代替內部數據結構。
內存映射數據結構可以為用戶節省大量的內存。不過,因為內存映射數據結構的編碼和操作方式要比內部數據結構要復雜得多,所以內存映射數據結構所占用的CPU 時間會比作用類似的內部數據結構要多。
這一部分將對Redis目前正在使用的兩種內存映射數據結構進行介紹。
參考: redis內存映射數據結構
整數集合(intset):用于有序、無重復地保存多個整數值,它會根據元素的值,自動選擇該用什么長度的整數類型來保存元素。
壓縮列表(Ziplist):是由一系列特殊編碼的連續內存塊組成的順序型(sequential)數據結構,它可以保存字符數組或整數值,它還是哈希鍵、列表鍵和有序集合鍵的底層實現之一。
5. 整數集合
整數集合(intset)是集合鍵的底層實現之一: 當一個集合只包含整數值元素, 并且這個集合的元素數量不多時, Redis 就會使用整數集合作為集合鍵的底層實現。
舉個例子, 如果我們創建一個只包含五個元素的集合鍵, 并且集合中的所有元素都是整數值, 那么這個集合鍵的底層實現就會是整數集合:
redis> SADD numbers 1 3 5 7 9
(integer) 5
redis> OBJECT ENCODING numbers
"intset"
6. 壓縮列表
壓縮列表(ziplist)是列表鍵和哈希鍵的底層實現之一。
當一個列表鍵只包含少量列表項, 并且每個列表項要么就是小整數值, 要么就是長度比較短的字符串, 那么 Redis 就會使用壓縮列表來做列表鍵的底層實現。
比如說, 執行以下命令將創建一個壓縮列表實現的列表鍵:
redis> RPUSH lst 1 3 5 10086 "hello" "world"
(integer) 6
redis> OBJECT ENCODING lst
"ziplist"
因為列表鍵里面包含的都是 1
、 3
、 5
、 10086
這樣的小整數值, 以及 "hello"
、 "world"
這樣的短字符串。
另外, 當一個哈希鍵只包含少量鍵值對, 并且每個鍵值對的鍵和值要么就是小整數值, 要么就是長度比較短的字符串, 那么 Redis 就會使用壓縮列表來做哈希鍵的底層實現。
Redis五種基本數據類型
- String——字符串
- Hash——字典
- List——列表
- Set——集合
- Sorted Set——有序集合
1. String——字符串
String 數據結構是簡單的 key-value 類型,value 不僅可以是 String,也可以是數字(當數字類型用 Long 可以表示的時候encoding 就是整型,其他都存儲在 sdshdr 當做字符串)。
字符串編碼
字符串類型分別使用
REDIS_ENCODING_INT
和REDIS_ENCODING_RAW
兩種編碼:
REDIS_ENCODING_INT
使用long
類型來保存long
類型值。REDIS_ENCODING_RAW
則使用sdshdr
結構來保存sds
(也即是char*
)、long long
、double
和long double
類型值。
換句話來說,在 Redis 中,只有能表示為long
類型的值,才會以整數的形式保存,其他類型的整數、小數和字符串,都是用sdshdr
結構來保存。
(字符串)是 Redis 使用得最為廣泛的數據類型,它除了是 SET 、 GET等命令的操作對象之外,數據庫中的所有鍵,以及執行命令時提供給 Redis 的參數,都是用這種類型保存的。
2. Hash——字典
在 Memcached 中,我們經常將一些結構化的信息打包成 hashmap,在客戶端序列化后存儲為一個字符串的值(一般是 JSON 格式),比如用戶的昵稱、年齡、性別、積分等。這時候在需要修改其中某一項時,通常需要將字符串(JSON)取出來,然后進行反序列化,修改某一項的值,再序列化成字符串(JSON)存儲回去。簡單修改一個屬性就干這么多事情,消耗必定是很大的,也不適用于一些可能并發操作的場合(比如兩個并發的操作都需要修改積分)。而 Redis 的 Hash 結構可以使你像在數據庫中 Update 一個屬性一樣只修改某一項屬性值。
(哈希表)是 HSET 、 HLEN等命令的操作對象,它使用
REDIS_ENCODING_ZIPLIST
和REDIS_ENCODING_HT
兩種編碼方式
https://www.kancloud.cn/kancloud/redisbook-first/63788
字典編碼的哈希表
當哈希表使用字典編碼時,程序將哈希表的鍵(key)保存為字典的鍵,將哈希表的值(value)保存為字典的值。哈希表所使用的字典的鍵和值都是字符串對象。
壓縮列表編碼的哈希表
當使用 REDIS_ENCODING_ZIPLIST 編碼哈希表時,程序通過將鍵和值一同推入壓縮列表,從而形成保存哈希表所需的鍵-值對結構。
image.png
當創建新的哈希表時,默認是使用壓縮列表作為底層數據結構的,因為省內存呀。只有當觸發了閾值才會轉為字典
哈希表中某個鍵或者值的長度大于server.hash_max_ziplist_value(默認為64)
壓縮列表中的節點數量大于server.hash_max_ziplist_entries(默認為512)
3. List——列表
List 說白了就是鏈表(redis 使用雙端鏈表實現的 List),相信學過數據結構知識的人都應該能理解其結構。使用 List 結構,我們可以輕松地實現最新消息排行等功能(比如新浪微博的 TimeLine )。List 的另一個應用就是消息隊列,可以利用 List 的 *PUSH 操作,將任務存在 List 中,然后工作線程再用 POP 操作將任務取出進行執行。Redis 還提供了操作 List 中某一段元素的 API,你可以直接查詢,刪除 List 中某一段的元素
REDIS_LIST
(列表)是 LPUSH 、 LRANGE 等命令的操作對象,它使用REDIS_ENCODING_ZIPLIST
和REDIS_ENCODING_LINKEDLIST
這兩種方式編碼:
創建新列表時 Redis 默認使用 REDIS_ENCODING_ZIPLIST 編碼,當以下任意一個條件被滿足時,列表會被轉換成 REDIS_ENCODING_LINKEDLIST 編碼:
試圖往列表新添加一個字符串值,且這個字符串的長度超過 server.list_max_ziplist_value (默認值為 64 )。
ziplist 包含的節點超過 server.list_max_ziplist_entries (默認值為 512 )。
因為列表本身的操作和底層實現基本一致,講講阻塞(列表,其實就是隊列):
BLPOP 、 BRPOP 和 BRPOPLPUSH lpush] 三個命令都可能造成客戶端被阻塞,將這些命令統稱為列表的阻塞原語。
比如,POP命令是刪除一個節點,那么當沒有節點的時候,客戶端會阻塞直到一個元素添加進來,然后再執行POP命令。
參考:https://www.kancloud.cn/kancloud/redisbook-first/63789
4. Set——集合
Set 就是一個集合,集合的概念就是一堆不重復值的組合。利用 Redis 提供的 Set 數據結構,可以存儲一些集合性的數據。比如在微博應用中,可以將一個用戶所有的關注人存在一個集合中,將其所有粉絲存在一個集合。因為 Redis 非常人性化的為集合提供了求交集、并集、差集等操作,那么就可以非常方便的實現如共同關注、共同喜好、二度好友等功能,對上面的所有集合操作,你還可以使用不同的命令選擇將結果返回給客戶端還是存集到一個新的集合中。
REDIS_SET
(集合)是 SADD 、 SRANDMEMBER 等命令的操作對象,它使用REDIS_ENCODING_INTSET
和REDIS_ENCODING_HT
兩種方式編碼:
編碼的選擇
第一個添加到集合的元素,決定了創建集合時所使用的編碼:
如果第一個元素可以表示為 long long 類型值(也即是,它是一個整數), 那么集合的初始編碼為 REDIS_ENCODING_INTSET 。
否則,集合的初始編碼為 REDIS_ENCODING_HT 。
編碼的切換
如果一個集合使用 REDIS_ENCODING_INTSET 編碼,那么當以下任何一個條件被滿足時,這個集合會被轉換成 REDIS_ENCODING_HT 編碼:
intset 保存的整數值個數超過 server.set_max_intset_entries (默認值為 512 )。
試圖往集合里添加一個新元素,并且這個元素不能被表示為 long long 類型(也即是,它不是一個整數)。
Redis 集合類型命令的實現,主要是對 intset 和 dict 兩個數據結構的操作函數的包裝,以及一些在兩種編碼之間進行轉換的函數
5. Sorted Set——有序集合
和Sets相比,Sorted Sets是將 Set 中的元素增加了一個權重參數 score,使得集合中的元素能夠按 score 進行有序排列,比如一個存儲全班同學成績的 Sorted Sets,其集合 value 可以是同學的學號,而 score 就可以是其考試得分,這樣在數據插入集合的時候,就已經進行了天然的排序。另外還可以用 Sorted Sets 來做帶權重的隊列,比如普通消息的 score 為1,重要消息的 score 為2,然后工作線程可以選擇按 score 的倒序來獲取工作任務。讓重要的任務優先執行。
應用場景:
1.帶有權重的元素,比如一個游戲的用戶得分排行榜
2.比較復雜的數據結構,一般用到的場景不算太多
(有序集)是 ZADD 、 ZCOUNT 等命令的操作對象,它使用 REDIS_ENCODING_ZIPLIST 和 REDIS_ENCODING_SKIPLIST 兩種方式編碼:
編碼的轉換
對于一個 REDIS_ENCODING_ZIPLIST 編碼的有序集,只要滿足以下任一條件,就將它轉換為 REDIS_ENCODING_SKIPLIST 編碼:
ziplist 所保存的元素數量超過服務器屬性 server.zset_max_ziplist_entries 的值(默認值為 128 )
新添加元素的 member 的長度大于服務器屬性 server.zset_max_ziplist_value 的值(默認值為 64 )
了解兩種編碼實現的方式和效率(還是比較有意思),請參考:https://www.kancloud.cn/kancloud/redisbook-first/63791
6. 對象處理機制(RedisObject)
分析:
在 Redis 的命令中,用于對鍵(key)進行處理的命令占了很大一部分,而對于鍵所保存的值的類型(后簡稱“鍵的類型”),鍵能執行的命令又各不相同。
比如說,LPUSH 和 LLEN 只能用于列表鍵,而 SADD 和 SRANDMEMBER 只能用于集合鍵,等等。
另外一些命令,比如 DEL 、 TTL 和 TYPE ,可以用于任何類型的鍵,但是,要正確實現這些命令,必須為不同類型的鍵設置不同的處理方式:比如說,刪除一個列表鍵和刪除一個字符串鍵的操作過程就不太一樣。
以上的描述說明,Redis 必須讓每個鍵都帶有類型信息,使得程序可以檢查鍵的類型,并為它選擇合適的處理方式。
另外,在前面介紹各個底層數據結構時有提到,Redis 的每一種數據類型,比如字符串、列表、有序集,它們都擁有不只一種底層實現(Redis 內部稱之為編碼,encoding),這說明,每當對某種數據類型的鍵進行操作時,程序都必須根據鍵所采取的編碼,進行不同的操作。
比如說,集合類型就可以由字典和整數集合兩種不同的數據結構實現,但是,當用戶執行 SADD命令時,他/她應該不必關心集合使用的是什么編碼,只要 Redis 能按照 SADD 命令的指示,將新元素添加到集合就可以了。
這說明,操作數據類型的命令除了要對鍵的類型進行檢查之外,還需要根據數據類型的不同編碼進行多態處理。
結論:
為了解決以上問題,Redis 構建了自己的類型系統,這個系統的主要功能包括:
- redisObject 對象。
- 基于 redisObject 對象的類型檢查。
- 基于 redisObject 對象的顯式多態函數。
- 對 redisObject 進行分配、共享和銷毀的機制。
數據結構:
redisObject 是 Redis 類型系統的核心,數據庫中的每個鍵、值,以及 Redis 本身處理的參數,都表示為這種數據類型。
redisObject 的定義位于 redis.h :
/*
* Redis 對象
*/
typedef struct redisObject {
// 類型
unsigned type:4;
// 對齊位
unsigned notused:2;
// 編碼方式
unsigned encoding:4;
// LRU 時間(相對于 server.lruclock)
unsigned lru:22;
// 引用計數
int refcount;
// 指向對象的值
void *ptr;
} robj;
type 、 encoding 和 ptr 是最重要的三個屬性。
type 記錄了對象所保存的值的類型,它的值可能是以下常量的其中一個(定義位于 redis.h):
/*
對象類型
*/
#define REDIS_STRING 0 // 字符串
#define REDIS_LIST 1 // 列表
#define REDIS_SET 2 // 集合
#define REDIS_ZSET 3 // 有序集
#define REDIS_HASH 4 // 哈希表
encoding 記錄了對象所保存的值的編碼,它的值可能是以下常量的其中一個(定義位于 redis.h):
/*
對象編碼
*/
#define REDIS_ENCODING_RAW 0 // 編碼為字符串
#define REDIS_ENCODING_INT 1 // 編碼為整數
#define REDIS_ENCODING_HT 2 // 編碼為哈希表
#define REDIS_ENCODING_ZIPMAP 3 // 編碼為 zipmap
#define REDIS_ENCODING_LINKEDLIST 4 // 編碼為雙端鏈表
#define REDIS_ENCODING_ZIPLIST 5 // 編碼為壓縮列表
#define REDIS_ENCODING_INTSET 6 // 編碼為整數集合
#define REDIS_ENCODING_SKIPLIST 7 // 編碼為跳躍表
ptr 是一個指針,指向實際保存值的數據結構,這個數據結構由 type 屬性和 encoding 屬性決定。
小結
- Redis 使用自己實現的對象機制來實現類型判斷、命令多態和基于引用計數的垃圾回收。
- 一種 Redis 類型的鍵可以有多種底層實現。
- Redis 會預分配一些常用的數據對象,并通過共享這些對象來減少內存占用,和避免頻繁地為小對象分配內存