Redis 数据结构
最近接触到了Redis的使用,借这个机会深入的了解一下Redis的实现和设计原理。下面先介绍一下Redis底层所用到的数据结构。Redis的实现几乎都是基于下面的几个数据结构之上的。
1.SDS 结构
struct sdshdr {
int len; // 记录数组中已经使用的字节数量,不算结尾的\0
int free; // 记录数组中还剩余的字节数量
int buf[]; // 保存字符串
};
2.SDS与c语言字符串区别
- C语言中求字符串长度的时间复杂度是O(N), SDS时间复杂度是O(1)
- 杜绝缓冲区溢出,像strcat函数将src字符串拼接到dest,C字符串不记录字符串长度,所以strcat函数假设dest分配了足够多的内存可以容纳src的所有内容。但是一旦假设不成立,就会产生缓冲区溢出。SDS有一个sdscat函数在执行拼接之间会检查长度,如果不够则会先扩展空间。
- 减少修改字符串时(例如:append)带来的内存重新分类次数,(1).SDS空间预分配,如果len小于1MB,程序会分配与len相同大小的free空间。如果len>1MB,则会分配1MB的free空间。(2).惰性空间释放,当需要缩短SDS保存的字符串时,程序用free保存这些空间,待将来使用。
- 二进制安全,C字符串必须符合某种编码(例如:ASCII),除了字符串结尾之外,字符串中不能有空字符(\0),这样限制C字符串只能保存文本数据,不能保存图片音频等。
- 兼容部分C字符串函数
3.链表结构
struct listNode {
struct listNode* prev;
struct listNode* next;
void* value;
};
struct list {;
listNode *head; //头节点
listNode *tail; //尾节点
unsigned long len; //链表长度
void *(*dup) (void *ptr); //节点复制函数, 用于复制链表保存的值
void (*free) (void *ptr); //节点释放函数, 用户释放链表保存的值
int (*match) (void *ptr, void *key); // 节点对比函数
};
4.链表特性
- 双端队列
- 无环
- 带有头指针和尾指针
- 链表长度计数器
- 多态:节点使用void*指针保存节点值,可以使用dup, free, match三个属性为节点设置特定函数,所以链表可以用来保存不同类型的值
5.字典数据结构
struct dictEntry {
void *key;
union{ //保存值,可以是一个指针,无符号64位整数,有符号64位整数
void *val;
uint64_tu64;
int64_ts64;
}v;
struct dictEntry* next;
};
struct dictht{
dictEntry **table; //哈希表数组
unsigned long size; //哈希表大小
unsigned long sizemask;// 哈希表大小掩码,用于计算索引值 总是等于size-1
unsigned long used; //该哈希表已有节点数量
};
字典的数据结构:
struct dict {
dictType* type; //类型特定函数,用于支持多态字典
void *privdata;//私有数据,用于支持多态字典
dictht ht[2];//哈希表, 一般只使用ht[0], ht[1]是在rehash时使用
int trehashidx; //rehash索引 当rehab不在进行时,值为-1,记录rehash的进度
};
struct dictType {
unsigned int (*hashFunction)(const void *key); //计算哈希值的函数
void *(*keyDup) (void *privdata, const void *key);//复制键的函数
void *(*valDup) (void *privdata, const void *obj); //复制值的函数
void *(*keyCompare) (void *privdata, const void *key1, const void *key2);//对比键的函数
void (*keyDestructor) (void *privdata, void *key);//销毁键的函数
void (*valDestructor) (void *privdata, void *obj);//销毁值函数
};
6.rehash
随着操作不短执行,哈希表保存的键值对会主键增多或减少,为了让哈希表的敷在引资维持在一个合理的范围之内,当哈希表保存的键值对数量太多或太少,程序会对哈希表大小进行相应的扩展或收缩。步骤如下:
- 为字典的ht[1]分配哈希表空间,这个哈希表空间大小的取决因素:(1). 如果是执行扩展操作,ht[1]的大小为第一个大于等于ht[0].used * 2 的2的n次幂。 (2).如果是执行收缩操作,那么ht[1]的大小硕士第一个大于等于ht[0].used的2的n次幂。
- 将ht[0]所有键值对rehash到ht[1]
- 释放ht[0],将ht[1]设置为ht[0]。并在ht[1]新创建一个空哈希表
7.哈希表扩展和收缩的时机
当满足下面任意个条件是,程序会自动对哈希表执行扩展操作
- 服务器没有在执行BGSAVE或者BGREWRITEAOF命令,并且哈西包负载因子大于1
- 服务器正在执行BGSAVE命令或者BGREWRITEAOF命令,且哈希的负载因子大于5
哈希因子计算公式:
load_factor = ht[0].used / ht[0].size
8.哈希表仅仅是rehash
哈希表如果非常大的话,如果一次性对ht[0] rehash 到 ht[1] 会花费很长一段时间,可能会造成服务暂停一段时间,这在线上是不可接受的,因此rehash是渐进式的。
- 为ht[1]分配空间
- 在字典中维持一个计数变量rehashidx,并设置为0,表示rehash正式开始
- rehash期间,每次对字典的操作(添加,删除,查找,更新)以外,顺带将ht[0]哈希表中的rehashidx索引上的所有键值对rehash到ht[1]并将rehashidx属性加一
- 随着字典操作的不断执行。最终会在一个时间点上,ht[0]所有的兼职都会被rehash至ht[1]
为了保证数据一致性,在渐进式rehash过程中所有操作会对两个哈希表同时进行操作。
9.跳跃表
zskiplist结构:
- header 指向跳跃表头结点
- tail 指向尾节点
- level 纪录目前跳跃表内,层数最大的那个节点的层数(表头节点的层数不计算在内)
- length 纪录跳跃表长度,也就是跳跃表目前包含节点数量
如上图,zskiplist结构右方是四个zskiplistNode结构,该结构包含一下属性:
- level 节点中用L1, L2 L3等标记节点的层。L1表示第一层。 每个层都带有两个属性,(1).前进指针 (2). 跨度。前进指针用于访问位于表尾方向的其他节点,跨度则记录了前进指针所指向的节点和当前节点的距离。图中带有数字的箭头就表示前进指针。
- 头退指针 backward: BW字样编辑后退指针。他指向前一个节点
- score,节点按照各自保存的分支由小到大排列
- obj: 个个节点中o1, o2是节点所保存的成员对象
表头几点也包含score backward 只是不被用到所以在图中省了。
typedef struct zskiplistNode {
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned int span;
}level[];
struct zskiplistNode *backward;
double score;
robj * obj;
}zskiplistNode;
level(层) ,一般来说曾德数量月耳朵,访问其他节点速度就越快,每次创建一个新跳跃表节点,程序都根据幂次定律(越大的书出现的概率越小)随机生成一个介于1到32的值作为level数组大小,这个大小就是层的高度。
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
}zskiplist;
10.整数集合
是集合键的底层实现,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis就会使用正数集合作为集合键的底层实现。其数据结构如下:
typedef struct intset {
uint32_t encoding; //编码方式
uint32_t length; // 集合包含元素数量
int8_t contents[]; //数据, 是按照由小到大的顺序排列的,不包含重复数字
}intset;
集合升级, 当讲一个新元素添加到整数集合中,并且新元素类型比整数集合现有所有类型都要长时,整数集合需要升级。然后才将新元素添加到整数集合中。但是整数集合不支持类型降级操作。