一、redis中數據對象
redis有五大數據類型, 通過統一對象redisObject存儲, redisObject的結構主要包含以下部分:
- type屬性存儲對象的類型, 也就是string, list, hash, set, zset中的一種。可以通過type命令查看。
- encoding屬性記錄對象的所使用的編碼,即底層數據結構。
- ptr 指向底層數據結構的指針。
1 typedef struct redisObject {
2 // 類型
3 unsigned type:4;
4 // 編碼
5 unsigned encoding:4;
6 // 指向底層實現數據結構的指針
7 void *ptr;
8 // ...
9 } robj;
redis為每種數據類型,提供了兩種以上的底層數據結構實現,可以通過type和object encoding這兩個命令查看數據對象的類型和數據結構。
127.0.0.1:6379> set key1 100
OK
127.0.0.1:6379> type key1
string
127.0.0.1:6379> object encoding key1
"int"
127.0.0.1:6379> set key2 'abc'
OK
127.0.0.1:6379> type key2
string
127.0.0.1:6379> object encoding key2
"embstr"
二、不同數據類型對應的底層數據結構
- 字符串
- int:8個字節(jié)的長整型。
- embstr:小于等于39個字節(jié)的字符串。
- raw:大于39個字節(jié)的字符串。
Redis會根據當前值的類型和長度決定使用哪種內部編碼實現。
- 哈希
- ziplist(壓縮列表):當哈希類型元素個數小于hash-max-ziplist-entries 配置(默認512個)、同時所有值都小于hash-max-ziplist-value配置(默認64 字節(jié))時,Redis會
使用ziplist作為哈希的內部實現,ziplist使用更加緊湊的 結構實現多個元素的連續(xù)存儲,所以在節(jié)省內存方面比hashtable更加優(yōu)秀。 - hashtable(哈希表):當哈希類型無法滿足ziplist的條件時,Redis會使 用hashtable作為哈希的內部實現,因為此時ziplist的讀寫效率會下降,
而 hashtable的讀寫時間復雜度為O(1)。
- 列表
- ziplist(壓縮列表):當列表的元素個數小于list-max-ziplist-entries配置 (默認512個),同時列表中每個元素的值都小于list-max-ziplist-value配置時 (默認64字節(jié)),
Redis會選用ziplist來作為列表的內部實現來減少內存的使 用。 - linkedlist(鏈表):當列表類型無法滿足ziplist的條件時,Redis會使用 linkedlist作為列表的內部實現。
- quicklist ziplist和linkedlist的結合, 以ziplist為節(jié)點的鏈表(linkedlist)
- 集合
- intset(整數集合):當集合中的元素都是整數且元素個數小于set-max- intset-entries配置(默認512個)時,Redis會選用intset來作為集合的內部實 現,從而減少內存的使用。
- hashtable(哈希表):當集合類型無法滿足intset的條件時,Redis會使 用hashtable作為集合的內部實現。
- 有序集合
- ziplist(壓縮列表):當有序集合的元素個數小于zset-max-ziplist- entries配置(默認128個),同時每個元素的值都小于zset-max-ziplist-value配 置(默認64字節(jié))時,
Redis會用ziplist來作為有序集合的內部實現,ziplist 可以有效減少內存的使用。 - skiplist(跳躍表):當ziplist條件不滿足時,有序集合會使用skiplist作 為內部實現,因為此時ziplist的讀寫效率會下降。
三、數據結構實現
1. int
int保存的是long類型的整數,8個字節(jié)長整型,直接保存在redisObject對象的ptr屬性中。
2. embstr
embstr保存小于等于39個字節(jié)的字符串,使用動態(tài)字符串(SDS)實現。直接保存在redisObject對象的ptr屬性中,只需要分配一次內存, 即創(chuàng)建redisObject對象。
3. raw
raw保存大于39個字節(jié)的字符串,使用動態(tài)字符串(SDS)實現,需要單獨分配內存給sdshdr(SDS)結構。
注意: 在redis 3.2之后,embstr和raw改為以44字節(jié)為分界線
SDS
redis沒有采用c的字符串結構,而是構建了動態(tài)字符串的數據結構,先來看看結構源碼:
struct sdshdr{
//記錄buf數組中已使用字節(jié)的數量
//等于 SDS 保存字符串的長度
int len;
//記錄 buf 數組中未使用字節(jié)的數量
int free;
//字節(jié)數組,用于保存字符串,最后一個位置存儲的是空字符'\0', 不計入len
char buf[];
}
SDS的優(yōu)化:
- 空間預分配,不用擔心字符串變更造成的內存溢出。
- 如果空間夠用,則不會額外分配空間, 通過free屬性。
- 如果修改后的 SDS 長度 len 小于 1MB,那么程序分配和 len 屬性相等的未使用空間,此時 free 和 len 的值相同。所以此時數組的實際長度為 free + len + 1byte(額外的空字符 1 個字節(jié))。
- 如果修改后的 SDS 長度大于 1MB,那么程序分配 1MB 的未使用空間。實際長度為 len + 1MB + 1byte。
- 惰性空間釋放,字符串縮短,并不是立即重新分配內存,釋放空間。
- 常數時間復雜度讀取字符串長度, 通過len屬性
- 二進制安全, c語言中空字符意味著字符串結束,SDS則不需要考慮,通過len判斷是否結束。
分界線39或44的來源
- Redis中內存分配使用的是 jemalloc,jemalloc 分配內存的時候是按照 8、16、32、64 作為 chunk 的單位進行分配的。
- 為了保證采用embstr編碼方式的字符串能被 jemalloc 分配在同一個 chunk 中,整個redisObject不超過64
- 因此OBJ_ENCODING_EMBSTR_SIZE_LIMIT = 64 - 16(redisObject) - 4(sdshdr的len屬性) - 4(sdshdr的free屬性) - 1(sdshdr的buf最后一位'\0') = 39。
- len和size之前使用unsign int,占4個字節(jié),調整為調整為unit8_t, 占1個字節(jié)。并追加了一個char flag 1個字節(jié)。
4. ziplist
壓縮列表是 Redis 為了節(jié)約內存而實現的,是一系列特殊編碼的連續(xù)內存塊組成的順序型數據結構。 結構如下圖:
- zlbytes 4字節(jié),記錄整個壓縮列表占用的內存字節(jié)數。
- zltail 4字節(jié),記錄壓縮列表表尾節(jié)點的位置。
- zllen 2字節(jié),記錄壓縮列表節(jié)點個數。超過需要65535遍歷。
- entry 列表節(jié)點,長度不定,由內容決定。
-
zlend 1字節(jié),0xFF 標記壓縮的結束。
壓縮列表的節(jié)點結構如下圖:
- privious_entry_length占四個字節(jié),取決于前一個節(jié)點的長度,小于254字節(jié),就是占1個字節(jié),否則就是5個字節(jié)。
- enncoding 記錄節(jié)點的content保存數據的類型和長度。
壓縮列表的遍歷:
通過指向表尾節(jié)點的位置指針p1, 減去節(jié)點的previous_entry_length,得到前一個節(jié)點起始地址的指針。如此循環(huán),從表尾遍歷到表頭節(jié)點。
5. linkedlist
雙向鏈表結構,表頭節(jié)點的前置節(jié)點和表尾節(jié)點的后置節(jié)點都是NULL,是無環(huán)鏈表。鏈表結構源碼如下:
typedef struct list{
// 表頭節(jié)點
listNode *head;
// 表尾節(jié)點
listNode *tail;
// 鏈表節(jié)點數
unsigned long len;
// 節(jié)點值復制函數
void (*free) (void *ptr);
// 節(jié)點值釋放函數
void (*free) (void *ptr);
// 節(jié)點值對比函數
int (*match) (void *ptr,void *key);
}list;
6. quicklist
一個由ziplist組成的雙向鏈表(ziplist和linklist結合),即鏈表的每個節(jié)點都是ziplist;redis3.2新增的數據結構,用于列表的實現。quicklist結構源碼:
typedef struct quicklist {
// 指向頭部quicklist節(jié)點的指針
quicklistNode *head;
// 指向尾部quicklist節(jié)點的指針
quicklistNode *tail;
// quicklist中所有ziplist中的entry節(jié)點數量。
unsigned long count;
// quicklist的鏈表節(jié)點數量
unsigned int len;
// 保存ziplist的大小,配置文件設定,占16bits
int fill : 16;
// 保存壓縮程度值,配置文件設定,占16bits,0表示不壓縮
unsigned int compress : 16;
} quicklist;
7. intset
當一個集合中只有整數元素,就會使用intset結構。結構源碼:
typedef struct intset {
// 編碼方式
uint32_t encoding;
// 集合包含的元素數量
uint32_t length;
// 保存元素的數組
int8_t contents[];
} intset;
- encoding有三種屬性值:INTSET_ENC_INT16,INTSET_ENC_INT32, INTSET_ENC_INT64, 分別表示用int16_t, int32_t, int64_tL類型的數組,進行數據存儲。
當元素大小超過時,會進行升級。(不支持降級)
8. hashtable
redis的hash表,采用鏈地址法解決沖突。
哈希表結構:
typedef struct dict {
// 類型特定函數
dictType *type;
// 私有數據
void *privdata;
// 哈希表
dictht ht[2];
// rehash 索引
// 當 rehash 不在進行時,值為 -1
int rehashidx; /* rehashing not in progress if rehashidx == -1 */
} dict;
其中dictht ht[0]為正常情況下使用,ht[1]為rehash過程使用。
typedef struct dictht {
// 哈希表數組
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩碼,用于計算索引值, 總是等于 size - 1
unsigned long sizemask;
// 該哈希表已有節(jié)點的數量
unsigned long used;
} dictht;
hash節(jié)點結構:
typedef struct dictEntry {
// 鍵
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// 指向下個哈希表節(jié)點,形成鏈表
struct dictEntry *next; // 單鏈表結構
} dictEntry;
redis的hash節(jié)點采用單鏈表結構,哈希表沒有使用紅黑樹,對哈希節(jié)點進行優(yōu)化。
ps: 在hash沖突嚴重時,大量hashcode落到同一個hash節(jié)點上,此時hash表退化成單鏈表。
在JDK1.8中采用HashTable對進行優(yōu)化,當鏈表數量超過8時,使用紅黑樹替代鏈表。rehash 由負載因子決定什么時候進行rehash。過程如下:
- 為ht[1]分配空間,大小為比當前ht[0]已使用值的兩倍大的第一個2的整數冪。如,已使用空間7,則分配比7*2大的最近的2的整數冪,即16。
- 將ht[0]中所有鍵值對,rehash到ht[1]上。
- 完成遷移后,釋放ht[0], 將ht[1]設置為ht[0], 在ht[1]處新建空白哈希表,為下一次rehash做準備。
- 漸進式rehash 當鍵值對數量巨大,一次性全部rehash將造成阻塞,服務暫停。所以拆分成多次,慢慢的將ht[0]中的數據rehash到ht[1]中。過程如下:
- 為ht[1]分配空間,同時持有兩個哈希表(一個空表、一個有數據)。
- 維持計數器rehashidx,初始值為0,表示rehash開始。
- 每次增刪改查,都順帶將ht[0]中的數據遷移到ht[1], rehashidx++.
- 直到rehash完成,rehashidx設置為-1。
漸進式hash中,更新、刪除、查找都會在兩個hash表上進行。新增操作只在ht[1]上進行,保證ht[0]只減不增,直到成為空表。
(假如ht[0]有冷門數據一直不被操作,ht[0]一直沒有清空,ht[1]觸發(fā)新的rehash閾值怎么辦?)
9. skiplist跳躍表
跳躍表的結構如下:
typedef struct zskiplist {
// 表頭節(jié)點和表尾節(jié)點
struct zskiplistNode *header, *tail;
// 跳表節(jié)點的數量
unsigned long length;
// 跳表層數
int level;
} zskiplist;
節(jié)點結構如下:
typedef struct zskiplistNode {
// 后退指針
struct zskiplistNode *backward;
// 分數值
double score;
// 成員對象
robj *obj;
// 層
struct zskiplistLevel {
// 前進指針
struct zskiplistNode *forward;
// 跨度,記錄兩個節(jié)點間的距離
unsigned int span;
} level[];
} zskiplistNode;
- skiplist是一種以空間換取時間的結構。由于鏈表,無法進行二分查找,因此借鑒數據庫索引的思想,提取出鏈表中關鍵節(jié)點(索引),先在關鍵節(jié)點上查找,再進入下層鏈表查找。提取多層關鍵節(jié)點,就形成了跳躍表。
- 跳躍表在es的lucene索引也有有應用。
為什么用跳躍表不用平衡樹
- skiplist算法實現簡單得多。
- 跳躍表和平衡樹的插入刪除時間復雜度都是O(logn),不過平衡樹的插入和刪除操作引發(fā)樹結構的調整,操作復雜。skiplist只需要修改相鄰節(jié)點的指針,操作簡單。
- 查詢單個key,跳躍表和平衡樹的時間復雜度都是O(logn),大體相當。
- 范圍查找,平衡樹要復雜,skiplist適合zset各種操作。