摘要: ## 引言 Redis是一個key-value存儲系統。和Memcached類似,它支持存儲的value類型相對更多,包括string(字符串)、list(鏈表)、set(集合)、zset(sorted set有序集合)和hash(哈希類型)。這些數據類型都支持push/pop、add/remove以及取交集並集和差集及更豐富的操作,而且這些操作都是原子性的。 本文將對Redis數據的
引言
Redis是一個key-value存儲系統。和Memcached類似,它支持存儲的value類型相對更多,包括string(字符串)、list(鏈表)、set(集合)、zset(sorted set有序集合)和hash(哈希類型)。這些數據類型都支持push/pop、add/remove以及取交集並集和差集及更豐富的操作,而且這些操作都是原子性的。
本文將對Redis數據的編碼方式和底層數據結構進行分析和介紹,幫助讀者更好的瞭解和使用它們。
數據類型和編碼方式
Redis中數據對象的定義如下:
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
int refcount;
void *ptr;
} robj;
其中type對應了Redis中的五種基本數據類型:
\#define OBJ_STRING 0
\#define OBJ_LIST 1
\#define OBJ_SET 2
\#define OBJ_ZSET 3
\#define OBJ_HASH 4
encoding則對應了 Redis 中的十種編碼方式:
/* Objects encoding. Some kind of objects like Strings and Hashes can be
* internally represented in multiple ways. The 'encoding' field of the object
* is set to one of this fields for this object. */
\#define OBJ_ENCODING_RAW 0 /* Raw representation */
\#define OBJ_ENCODING_INT 1 /* Encoded as integer */
\#define OBJ_ENCODING_HT 2 /* Encoded as hash table */
\#define OBJ_ENCODING_ZIPMAP 3 /* Encoded as zipmap */ // 已廢棄
\#define OBJ_ENCODING_LINKEDLIST 4 /* Encoded as regular linked list */
\#define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
\#define OBJ_ENCODING_INTSET 6 /* Encoded as intset */
\#define OBJ_ENCODING_SKIPLIST 7 /* Encoded as skiplist */
\#define OBJ_ENCODING_EMBSTR 8 /* Embedded sds string encoding */
\#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */
OBJ_ENCODING_RAW
RAW編碼方式使用簡單動態字符串來保存字符串對象,其具體定義爲:
struct sdshdr {
unsigned int len;
unsigned int free;
char buf[];
};
從len字段可以判斷並不不依賴於'\0',故可以用與保存二進制對象。
從free字段可以判斷其空間分配是採用預分配的方式,避免字符串修改時頻繁分配釋放內存。具體分配算法爲:
sds sdsMakeRoomFor(sds s,size_t addlen) {
...
if (sdsavail(s) >= addlen) return s;
newlen = sdslen(s)+addlen;
if (newlen < SDS_MAX_PREALLOC)
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC;
sh = (void*) (s-(sizeof(struct sdshdr)));
newsh = zrealloc(sh,sizeof(struct sdshdr)+newlen+1);
if (newsh == NULL) return NULL;
newsh->free = newlen - len;
return newsh->buf;
}
OBJ_ENCODING_INT
INT編碼方式以整數保存字符串數據,僅限能用long類型值表達的字符串。
當robj中的LRU值沒有意義的時候(實例沒有設置maxmemory限制或者maxmemory-policy設置的淘汰算法中不計算LRU值時), 0-10000之間的OBJ_ENCODING_INT編碼的字符串對象將進行共享。
具體算法如下:
len = sdslen(s);
if (len <= 21 && string2l(s,len,&value)) {
/* 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_VOLATILE_LRU &&
server.maxmemory_policy != MAXMEMORY_ALLKEYS_LRU)) &&
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;
o->ptr = (void*) value;
return o;
}
}
OBJ_ENCODING_EMBSTR
從Redis 3.0版本開始字符串引入了EMBSTR編碼方式,長度小於OBJ_ENCODING_EMBSTR_SIZE_LIMIT的字符串將以EMBSTR方式存儲。
EMBSTR方式的意思是 embedded string ,字符串的空間將會和redisObject對象的空間一起分配,兩者在同一個內存塊中。
Redis中內存分配使用的是jemalloc,jemalloc分配內存的時候是按照8,16,32,64作爲chunk的單位進行分配的。爲了保證採用這種編碼方式的字符串能被jemalloc分配在同一個chunk中,該字符串長度不能超過64,故字符串長度限制OBJ_ENCODING_EMBSTR_SIZE_LIMIT = 64 - sizeof('\0') - sizeof(robj)爲16 - sizeof(struct sdshdr)爲8 = 39。
採用這個方式可以減少內存分配的次數,提高內存分配的效率,降低內存碎片率。
robj *createStringObject(const char *ptr,size_t len) {
if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT)
return createEmbeddedStringObject(ptr,len);
else
return createRawStringObject(ptr,len);
}
robj *createEmbeddedStringObject(const char *ptr,size_t len) {
robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr)+len+1);
o->type = OBJ_STRING;
o->encoding = OBJ_ENCODING_EMBSTR;
...
if (ptr) {
memcpy(sh->buf,ptr,len);
sh->buf[len] = '\0';
} else {
memset(sh->buf,0,len+1);
}
return o;
}
OBJ_ENCODING_ZIPLIST
鏈表(List),哈希(Hash),有序集合(Sorted Set)在成員較少,成員值較小的時候都會採用壓縮列表(ZIPLIST)編碼方式進行存儲。
這裏成員"較少",成員值"較小"的標準可以通過配置項進行配置:
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
list-max-ziplist-entries 512
list-max-ziplist-value 64
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
壓縮列表簡單來說就是一系列連續的內存數據塊,其內存利用率很高,但增刪改查效率較低,所以只會在成員較少,值較小的情況下使用。
其典型結構如下:
area |<---- ziplist header ---->|<----------- entries ------------->|<-end->|
size 4 bytes 4 bytes 2 bytes ? ? ? ? 1 byte
+---------+--------+-------+--------+--------+--------+--------+-------+
component | zlbytes | zltail | zllen | entry1 | entry2 | ... | entryN | zlend |
+---------+--------+-------+--------+--------+--------+--------+-------+
^ ^ ^
address | | |
ZIPLIST_ENTRY_HEAD | ZIPLIST_ENTRY_END
|
ZIPLIST_ENTRY_TAIL
area |<------------------- entry -------------------->|
+------------------+----------+--------+---------+
component | pre_entry_length | encoding | length | content |
+------------------+----------+--------+---------+
OBJ_ENCODING_LINKDEDLIST / OBJ_ENCODING_QUICKLIST
在Redis 3.2版本之前,一般的鏈表使用LINKDEDLIST編碼。
在Redis 3.2版本開始,所有的鏈表都是用QUICKLIST編碼。
兩者都是使用基本的雙端鏈表數據結構,區別是QUICKLIST每個節點的值都是使用ZIPLIST進行存儲的。
// 3.2版本之前
typedef struct list {
listNode *head;
listNode *tail;
void *(*dup)(void *ptr);
void (*free)(void *ptr);
int (*match)(void *ptr,void *key);
unsigned long len;
} list;
typedef struct listNode {
struct listNode *prev;
struct listNode *next;
void *value;
} listNode;
// 3.2版本
typedef struct quicklist {
quicklistNode *head;
quicklistNode *tail;
unsigned long count; /* total count of all entries in all ziplists */
unsigned int len; /* number of quicklistNodes */
int fill : 16; /* fill factor for individual nodes */
unsigned int compress : 16; /* depth of end nodes not to compress;0=off */
} quicklist;
typedef struct quicklistNode {
struct quicklistNode *prev;
struct quicklistNode *next;
unsigned char *zl;
unsigned int sz; /* ziplist size in bytes */
unsigned int count : 16; /* count of items in ziplist */
unsigned int encoding : 2; /* RAW==1 or LZF==2 */
unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */
unsigned int recompress : 1; /* was this node previous compressed? */
unsigned int attempted_compress : 1; /* node can't compress; too small */
unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;
OBJ_ENCODING_INTSET
整數集合(intset)是集合鍵的底層實現之一:
當一個集合只包含整數值元素, 並且這個集合的元素數量不多時, Redis 就會使用整數集合作爲集合鍵的底層實現。
robj *setTypeCreate(robj *value) {
if (isObjectRepresentableAsLongLong(value,NULL) == REDIS_OK)
return createIntsetObject();
return createSetObject();
}
int setTypeAdd(robj *subject,robj *value) {
...
if (subject->encoding == REDIS_ENCODING_INTSET) {
if (isObjectRepresentableAsLongLong(value,&llval) == REDIS_OK) {
...
} else {
// 當往INTSET裏面插入非整數值時,會將集合轉成普通的HT編碼方式
setTypeConvert(subject,REDIS_ENCODING_HT);
...
}
...
}
...
}
OBJ_ENCODING_HT
字典是Redis中存在最廣泛的一種數據結構不僅在哈希對象,集合對象和有序結合對象中都有使用,而且Redis所有的Key,Value都是存在db->dict這張字典中的。
Redis 的字典使用哈希表作爲底層實現。
一個哈希表裏面可以有多個哈希表節點,而每個哈希表節點就保存了字典中的一個鍵值對。
哈希表的定義如下:
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
// Redis 的哈希表使用鏈地址法(separate chaining)來解決鍵衝突:
// 每個哈希表節點都有一個 next 指針, 多個哈希表節點可以用 next 指針構成一個單向鏈表,
// 被分配到同一個索引上的多個節點可以用這個單向鏈表連接起來, 這就解決了鍵衝突的問題。
struct dictEntry *next;
} dictEntry;
/* This is our hash table structure. */
typedef struct dictht {
dictEntry **table;
unsigned long size;
unsigned long sizemask;
unsigned long used;
} dictht;
typedef struct dictType {
// 用於計算Hash的函數指針
unsigned int (*hashFunction)(const void *key);
void *(*keyDup)(void *privdata,const void *key);
void *(*valDup)(void *privdata,const void *obj);
int (*keyCompare)(void *privdata,const void *key1,const void *key2);
void (*keyDestructor)(void *privdata,void *key);
void (*valDestructor)(void *privdata,void *obj);
} dictType;
Redis計算Hash值和索引的方式爲:
hash = dict->type->hashFunction(key);
index = hash & dict->ht[x].sizemask;
一個字典裏面保存了兩個hash表結構,用於哈希表的擴展收縮操作(Rehash)。
/* Every dictionary has two of this as we
* implement incremental rehashing,for the old to the new table.
typedef struct dict {
dictType *type;
void *privdata;
dictht ht[2];
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
int iterators; /* number of iterators currently running */
} dict;
隨着操作的不斷執行,哈希表保存的鍵值對會逐漸地增多或者減少,爲了讓哈希表的負載(used/size)維持在一個合理的範圍之內,當哈希表保存的鍵值對數量太多或者太少時,程序需要對哈希表的大小進行相應的擴展或者收縮。
具體算法爲:
- 爲字典的 ht[1] 哈希表分配空間,這個哈希表的空間大小爲: 第一個大於等於(擴展時)或者小於等於(收縮時) ht[0].used * 2 的 2^n(2 的 n 次方冪);
- 將保存在 ht[0] 中的所有鍵值對 rehash 到 ht[1] 上面:rehash指的是重新計算鍵的哈希值和索引值,然後將鍵值對放置到 ht[1]哈希表的指定位置上;
- 當 ht[0] 包含的所有鍵值對都遷移到了 ht[1] 之後(ht[0] 變爲空表),釋放 ht[0] ,將 ht[1] 設置爲 ht[0],並在 ht[1] 新創建一個空白哈希表,爲下一次 rehash 做準備。
一次性完成 rehash 過程時間可能很長,Redis採用漸進式 rehash 的方式完成整個過程。
- 每次對字典執行添加、刪除、查找或者更新操作時,程序除了執行指定的操作以外,還會順帶將 ht[0] 哈希表在 rehashidx 索引上的所有鍵值對 rehash 到 ht[1] , 並將 rehashidx 的值增一;
- 直到整個 ht[0] 全部完成 rehash 後,rehashindex設爲-1,釋放 ht[0] , ht[1]置爲 ht[0], 在 ht[1] 創建一個新的空白表。
OBJ_ENCODING_SKIPLIST
跳躍表(SKIPLIST)編碼方式爲有序集合對象專用,有序集合對象採用了字典+跳躍表的方式實現。其定義如下:
typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset;
其中字典裏面保存了有序集合中member與score的鍵值對,跳躍表則用於實現按score排序的功能。
跳躍表以有序的方式在層次化的鏈表中保存元素,在一般情況下情況下效率和平衡樹媲美, 比起平衡樹來說,跳躍表的實現要簡單直觀得多。
一般的跳躍表結構如下圖所示:
跳躍表的基本原理是每一個節點創建的時候層數爲隨機值,層數越高的機率越小,Redis中每升高一層的概率爲1/4。也就是說,一個跳躍表裏,一般情況下,每一層的節點數爲下一次的 1/4,相當於是一個“隨機化”的平衡樹。
\#define ZSKIPLIST_P 0.25 /* Skiplist P = 1/4 */ macro
int zslRandomLevel(void) {
int level = 1;
while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
level += 1;
return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}
具體的算法在此就不詳細贅述了,與一般的跳躍表實現相比,有序集合中的跳躍表有以下特點:
- 允許重複的 score 值:多個不同的 member 的 score 值可以相同。
- 進行對比操作時,不僅要檢查 score 值,還要檢查 member:當 score 值可以重複時,單靠 score 值無法判斷一個元素的身份,所以需要連 member 域都一併檢查才行。
- 每個節點都帶有一個高度爲1層的後退指針,用於從表尾方向向表頭方向迭代:當執行 ZREVRANGE 或ZREVRANGEBYSCORE這類以逆序處理有序集的命令時,就會用到這個屬性。
/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
robj *obj;
double score;
// 後退指針,方便 zrev 系列的逆序操作
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;
結束
知其然,更要知其所以然。
只有深入瞭解了Redis數據的編碼方式和底層數據結構,我們才能更好的瞭解使用它。在做疑難問題分析和業務優化時,才能更加有的放矢。
原文鏈接:https://yq.aliyun.com/articles/63461