深入理解Redis:底层数据结构

简介

redis[1]是一个key-value存储系统。和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set --有序集合)和hash(哈希类型)。通常我们并不需要理解其底层数据结构,但如果能了解一下相关知识将会有助于我们更有效地使用Redis,并能够将这些知识应用到我们的工作中。

Redis内部实现如下数据结构[2,3,4,10]:

1 String

2 Hash Table

3 Doubly Linked List

4 Skip List

5 Zip List

6 Int Sets

7 Zip Maps (从2.6版本开始废弃)



Hast table[5]:

在Redis中,所有key-value对都存储在一个hash table中。这个Hash table是一个二维结构。其包括一个一维固定长度的数组,每个槽位上保存一个dictEntry对象。key计算hash值后按照这个定长数组求模,结果相同的key-balue通过链表保存在同一个槽位上,这样便形成了一个二维结构。需要说明的是,hash table中这个固定长度的数组能够根据key-value数量动态调整大小,细节说明请参看引用[5],这里不做更多说明。



这里再看一下dictEntry的定义,主要关注其中的联合体v的val成员:

struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
struct dictEntry *next;
} dictEntry;

val是一个类型为robj的数据结构,其中的type标示了当前的value的数据类型(即string、list、set、zset或者hash),encoding标示了当前value存储方式(即ziplist,String,hash table或者double linked list等)

struct redisObject {
unsigned type:4;
unsigned notused:2; /* Not used */
unsigned encoding:4;
unsigned lru:22; /* lru time (relative to server.lruclock) */
int refcount;
void *ptr;
} robj;

encoding目前支持的范围如下所示,具体可参考[1]源代码实现,其中的zipmap由于表示范围的限制已经在2.6版本中废弃,相关说明参见[6]

#define REDIS_ENCODING_RAW 0        /* Raw representation */
#define REDIS_ENCODING_INT 1 /* Encoded as integer */
#define REDIS_ENCODING_HT 2 /* Encoded as hash table */
#define REDIS_ENCODING_ZIPMAP 3 /* Encoded as zipmap */
#define REDIS_ENCODING_LINKEDLIST 4 /* Encoded as regular linked list */
#define REDIS_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
#define REDIS_ENCODING_INTSET 6 /* Encoded as intset */
#define REDIS_ENCODING_SKIPLIST 7 /* Encoded as skiplist */

五种数据类型的内部实现

Redis在收到客户端的请求后,为每一个参数创建一个robj对象,type定义为REDIS_STRING,encoding为REDIS_ENCODING_RAW。接下来Redis根据第一个robj对象(也就是命令名)查找对应的函数,并调用查找到的函数,命令执行过程可参考[7]。

String

如果一个String类型的value能够保存为整数,则将对应robj对象的encoding修改为REDIS_ENCODING_INT,将对应robj对象的ptr值改为对应的数值。如果不能转为整数,保持原有encoding为REDIS_ENCODING_RAW。

因此String类型的数据可能使用原始的字符串存储(实际为sds - Simple Dynamic Strings[9],对应encoding为REDIS_ENCODING_RAW)或者整数存储。

具体查看某一个key的encoding,参考Redis命令object[8]



下面是具体的例子:

redis 127.0.0.1:6379> set hello 1

OK

redis 127.0.0.1:6379> OBJECT ENCODING hello

"int"

redis 127.0.0.1:6379> set hello world

OK

redis 127.0.0.1:6379> OBJECT ENCODING hello

"raw"

List

List类型的key创建时使用zip list结构存储,robj对象的encoding字段设置为REDIS_ENCODING_ZIPLIST。zip list实现细节可参考[3]。概况来讲,zip list通过一个连续的内存块实现list结构,其中的每个entry节点头部保存前后节点长度信息,实现双向链表功能。这个头部可根据前后entry长度进行内存压缩,而如果直接使用指针的话则至少需要两个指针,对64位系统来说将占用16个字节,使用zip list时最好情况下只需要两个字节,这在具有大量list类型的key-value对且各个value较小的应用来说,可以节省大量内存。

当list的elem数小于配置值: hash-max-ziplist-entries 或者elem_value字符串的长度小于 hash-max-ziplist-value, 可以编码成 REDIS_ENCODING_ZIPLIST 类型存储,以节约内存;但由于在zip list添加和删除元素会涉及到数据移动,因此当list内容较多时,转而使用双向链表。双向链表的实现可参考数据结构相关教科书。

相关内存优化说明请参考[11]。

Hash

新建的Hash类型也使用ziplist存储value,保存数据过多时,转而使用hast table。

Set

创建Set类型的key-value时,如果value能够表示为整数,则使用intset类型保存value。intset使用和ziplist相似的实现方式保存整数[4]。数据量大时,切换为使用hash table保存各个value。

Zset

zset指排序的set,如果新建的zset包含value数大于配置或者value长度大于配置值[11],则直接使用hash table和skip list[12]存储value,skip list实现对value的排序;否则直接使用skip list存储value。Redis可以保存相同score的value值,其实现可参考源代码[1]以及文献[12],Redis是参考[12]中伪代码实现的。



本文只对Redis底层数据结构实现进行了简单归并汇总,各部分实现细节请参考引用链接即Redis源代码。本文内容基于Redis 2.6版本。

引用

[1] http://redis.io/

[2] http://*.com/questions/9625246/what-are-the-underlying-data-structures-used-for-redis

[3] 《Redis ziplist内部结构分析》, http://www.searchdatabase.com.cn/showcontent_60781.htm

[4] 《解读Redis中ziplist、zipmap、intset实现细节》, http://www.wzxue.com/%E8%A7%A3%E8%AF%BBredis%E4%B8%ADziplist%E5%AE%9E%E7%8E%B0%E7%BB%86%E8%8A%82/

[5] 《redis源代码分析 – hash table》,http://www.kuqin.com/database/20110904/264306.html

[6] zipmap zmlen is too short, https://github.com/antirez/redis/issues/188

[7] 《深入理解Redis:命令处理流程 》, http://blog.csdn.net/hanhuili/article/details/17339005

[8] http://redis.io/commands/object

[9] 《Redis sds数据结构实现分析》,http://www.searchdatabase.com.cn/showcontent_64553.htm

[10] 《Redis内存存储结构分析》,http://www.searchtb.com/2011/05/redis-storage.html

[11] http://redis.io/topics/memory-optimization

[12] http://homepage.divms.uiowa.edu/~ghosh/skip.pdf

上一篇:Log4delphi使用心得


下一篇:oracle-sql优化-通过分组和缓存减少不必要的读