今天主要介绍一下Redis中用到的底层数据结构,其主要包含6种,分别为动态字符串、链表、跳跃表、压缩链表、字典、整数集合。
1. 动态字符串
SDS
int len; //代表实际长
int free; // 代表 buf 中未使用的长度
char[] buf; // 实际存储东西的地方
优势:
- O(1) 时间获得长度
- 减少修改字符串长度时,内存充分配次数
- 可重用 C 里面部分关于字符串的函数
2. 链表
链表节点的实现
typedef struct listNode {
ListNode *prev
ListNode *next
void * value
}
链表的实现
typedef struct list {
listNode *head;
listNode *tail;
unsigned long len;
// 节点值复制函数
void *(*dup) (void *ptr)
// 节点值释放函数
void *(*free) (void *ptr)
// 节点值对比函数
void *(*match) (void * ptr)
}
通过以上结构,可以看出,Redis 中链表的几个特性,
- 双端
- 无环
- O(1)获得链表头部和链表尾部
- O(1)获得链表长度
- 多态: 因为链表节点中使用 void 来代表链表的值,因此其可以实现保存不同类型的值的方式,并且 list 结构中也提供了用于复制、释放、对比链表节点值的函数的指针。
3. 字典
redis 中字典采用 hash 表实现。
hash 表
hash 表定义如下:
typedef struct dictht {
dictEntry **table;//哈希表数组
unsigned long size; //哈希表大小
unsigned long sizemask;//哈希表大小掩码,用于计算索引值 ,总是等于size-1
unsigned long used;//该哈希表已有节点的数量
} dictht;
其中 table 是一个数组,数组的每一项是一个指向 dictEntry 结构的指针。sizemask 用于计算某个 hash 值对应的索引,通过求余运算可快速的到索引值(和 java 中 hashmap 计算索引值的方法一样)。dictEntry 结构定义如下:
typedef struct dictEntry {
void *key; //键
union{
void *val;
uint64_t u64;
int64_t s64;
} v; //值
struct dictEntry *next; //指向下个哈希表节点,形成链表
} dictEntry;
其中 键值对的值可以是一个指针或 uint64_t 或 int64_t 类型的值。
next 属性用来解决 hash 冲突(链地址法)。
字典结构
redis 中的字典结构如下:
typedef struct dict {
dictType *type; //类型特定函数
void *privdata;//私有数据
dictht ht[2]; //哈希表
// rehash索引
//当rehash不在进行时,值为-1
int rehashidx;
} dict;
ht 属性包含两个 hash 表,其中一个在 rehash 的时候配合 rehashidx使用。
type 属性和 privatedata 属性是针对不同类型的键值对而设立的。
- type
type属性是一个指向dictType结构的指针,每个dictType结构保存了一簇用于操作特定类型键值对的函数,Redis会为用途不同的字典设置不同的类型特定函数。
- privatedata
而privdata属性则保存了需要传给那些类型特定函数的可选参数。
typedef struct dictType {
//计算哈希值的函数
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;
4. 跳跃表
跳跃表是一种有序的数据结构,它通过在每个节点维护多个指向其他节点的 指针,从而达到快速访问其他节点的功能。Redis 中只有两个地方用到了跳跃表:
- 有序集合键
- 集群节点中用作内部数据结构
在 redis 实现中,跳跃表主要定义了两个结构,zskiplist 和 zskiplistnode 结构。
zskiplist 结构如下:
typedef struct zskiplist {
struct zskiplistNode* header, *tail // 头节 点,尾节点
unsigned long level // 节点的最大层级,表头节点不计算在内
int length// 长度,表头节点不计算在内
}
zskiplistnode 结构如下:
typedef struct zskiplistNode {
//层
zskiplistLevel {
//前进指针
struct zskiplistNode *forward;
//跨度
unsigned int span;
} levle[];
// 后退指针
struct zskiplistNode *backward;
// 分值
double score;
robj* obj;
}
- 后退指针
用于表尾向表头遍历访问 - 分值和成员
跳跃表中,各节点按照分值从小到大排列,分值相同的,按照成员对象的字典序排序。成员对象是一个指针,指向一个字符串对象 - level 数组
1 个节点分属于多个层,在每次生成新节点的时候,从 1 至 32(redis 中宏定义) 中随机生成一个值作为数组大小。
- 前进指针
每层的前进指针,用于向前遍历,也用于加快搜索速度 - 跨度
记录两个节点之间的距离,计算两个节点 的 rank,在查找某节点时,将遍历过程中访问过的层的跨度加起来即得到该节点的 rank。
5. 整数集合
typedef struct intset {
uint32_t encoding;
uint32_t length;
int8_t[] contents;
}
contents 是保存实际内容的数组,虽然其类型标明为 int8_t,但是其实际保存内容的类型根据 encoding 的格式来确定,目前,encoding 的格式有 INTSET_ENC_INT64 等。
整数集合存在升级的概念,但不存在降级的概念,即当新插入的数值,在原来 encoding 已经无法保存的情况下,整数集合会进行升级的操作,即提高 encoding ,来增大数组所能保存的值的范围,当进行 encoding 升级之后,无法进行降级操作了。 有以下几个优势:
- 提升灵活性:自动进行数值的管理,不会发生溢出的情况。
- 节约内存:从使用者的角度,当只保存 int16 的值的情况,其每个数占 2 个字节,当保存 大于 int16 的情况,其才进行升级操作。
6. 压缩链表
zip list,从名字就可以看出来,压缩链表是为了节约内存而开发的。其虽然叫链表,但是其使用的是内存中连续的内存块。
组成:压缩链表可以包含任意多个节点(entry),每个 entry 可以包含一个字节数组或整数值。
在压缩链表的那块连续内存块中,包含了以下 5 个部分:
- zlbytes:4 个字节,记录压缩链表使用了多少个字节。
- zltail:4 个字节,记录最后一个 entry 的偏移量,
- zllen:2 个字节,entry的数量,如果值超过了 UINT16_MAX 时,就需要遍历来得到 entry数目了。
- entryx:
- zlend:1 个字节,0xff,压缩链表的结束标志
每个 entry 呢,其包含了以下几个部分: - previous_entry_length: 1 个字节或 5 个字节,以字节为单位记录前一个 entry 的长度,1 字节的话,最大表示到 253 长度,当前一个 entry 大于等于 254 时,就用 5 字节表示了。
- encoding: 1 字节,2 字节或 5 字节,记录了 content 的属性及长度,当最高两位为 11 时,表示 content 的整数值,当为其他时,content 为字节数组,其余位的内容,表示了 content 的长度或者类型属性
- content:保存节点的真实内容
ps: 从前往后找节点,都是根据字节数来找的,nb。
可能出现的问题
- 连锁更新
previous_entry_length 值刚好都为 253,这时来了个 256 字节的 entry 作为头节点,后面的都得更新 previous_entry_length
本篇文章总结于黄健宏老师的《Redis 设计与实现》