最近学习看黄健宏先生写的《redis设计与实现》,开篇即介绍了redis的基础数据结构和数据对象,看了一遍后觉得还是有点没弄明白相互之间的关系,觉得很有必要整理一下自己的思绪。
1. redis基础数据结构
- 简单动态字符串 -SDS
- 双链表 -list
- 字典 -dicht(hashtable)
- 跳跃表 -skiplist
- 整数集合 -intset
- 压缩列表 -ziplist
2. redis基础数据对象
字符串对象
- raw类型
- int 型
- emstr型
列表对象
- ziplist
- linkedlist
哈希对象
- ziplist
- hashtable
集合对象
- intset
- hashtable
有序集合对象
- ziplist
- skiplist
3. 数据结构的详细说明
3.1 简单动态字符串 -SDS
简单动态字符串实际是对c语言char*的封装,redis实现于在sds.h中,类似于c++的string,以及stl的vector。是一种常用的封装。它是用如下的数据结构。
struct sdshdr {
int len; // buf 中已占用空间的长度
int free; // buf 中剩余可用空间的长度
char buf[]; // 数据空间
};
3.2 双链表 -list
任何一本数据结构的书上都会有关于单链表的详细介绍,双链表就是在单链表的基础上增加了一个指向前驱的指针。redis实现于adlist.h
typedef struct listNode {
struct listNode *prev; // 指向前一个节点
struct listNode *next; // 指向后一个节点
void *value; // 节点的值
} listNode;
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;
3.3 字典 -dicht(hashtable)
3.3.1哈希表的数据结构
typedef struct dictEntry { // 哈希表节点
void *key; // 键
union { // 值
void *val;
uint64_t u64;
int64_t s64;
} v;
struct dictEntry *next; // 指向下个哈希表节点,形成链表redis使用链地址法处理哈希碰撞,
//故在每一个节点中都存在一个指向下一个节点的哈希表节点的指针。
} dictEntry;
typedef struct dictht {
dictEntry **table; // 哈希表数组
unsigned long size; // 哈希表大小
// 哈希表大小掩码,用于计算索引值
// 总是等于 size - 1
unsigned long sizemask;
unsigned long used; // 该哈希表已有节点的数量
} dictht;
redis使用Murmurhash2算法来计算键的哈希值。该算法于2008年发明,详细介绍查看该算法的官网。
3.3.2 字典的数据结构。
typedef struct dict {
dictType *type; // 类型特定函数
void *privdata; // 私有数据
dictht ht[2]; // 哈希表
// rehash 索引
// 当 rehash 不在进行时,值为 -1
int rehashidx; /* rehashing not in progress if rehashidx == -1 */
// 目前正在运行的安全迭代器的数量
int iterators; /* number of iterators currently running */
} dict;
我们可以看到每个字典中包含了两个哈希表,一般情况下我们只是用ht[0],此时rehashidx为-1。当哈希表中元素增多,used接近于哈希表的大小时,需要对哈希表进行扩容。此时为rehash,redis使用一种渐进式rehash策略,即每次查询或者插入操作时,判断是否正在进行rehash,若正在进行,则转移部分ht[0]中的元素到ht[1]中,这样可以将转移操作分布到各次操作中,不会阻塞很长时间。当转移完成后,ht[1]赋值给ht[0],恢复rehashidx为-1;查找元素时,根据是否正在进行rehash,判断是否需要查询ht[2]。
3.4 跳跃表-skiplist
跳跃表是比较少见的一种数据结构,redis中用于两个地方,有序集合键和集群内部数据结构。下图是跳跃表的示例图(来自维基百科)。
网上也有很多跳跃表的说明,就不细说跳跃表了。
3.5 整数集合-intset
intset是集合键的底层实现之一,其数据结构定义在intset.h中。
typedef struct intset {
uint32_t encoding; // 编码方式
uint32_t length;// 集合包含的元素数量
int8_t contents[]; // 保存元素的数组
} intset;
其中encoding编码方式INTSET_ENC_INT16 ,INTSET_ENC_INT32,INTSET_ENC_INT64
从以上的数据结构定义可以很清楚的看出,不管使用哪种哪种编码,intset都是将数据序列化到contents中。当数据元素全是INTSET_ENC_INT16 类型时,可以使用较少的存储空间,但是当插入一个INTSET_ENC_INT32 类型是就需要升级了。
3.6 压缩集合-ziplist
压缩列表是列表键和哈希键的底层实现之一。在内存中开辟一块连续的存储单元。
非空 ziplist在内存中如下存储。第一排代表所占字节,第二排代表内存中数据。
4 | 4 | 2 | ? | ? | ? | ? | 2 |
---|---|---|---|---|---|---|---|
zlbytes | zltail | zlen | entry1 | entry2 | … | entryN | zlend |
4 对象与数据结构之间的实现关系
数据对象定义在redis.h中。type表示类型,encoding表示编码方式。
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;
4.1 字符串对象
- 当字符串中保存的为int型整数时,内部使用int编码。
- 当字符串中保存小于32字节字符串时使用emstr,emstr为只读,写时转换为raw。
- 非以上两种情况时使用raw编码。