底层实现结构
背景知识
void* 表明是一个多态指针,可以指向任何类型,可以理解为Object变量。
union可以理解为枚举(非常不准确),每次只会选择其中一个
struct可以理解为类
hashtable
数据结构
dict是一个总容器,放置着dictht,dictht就是哈希表,dictht的底层结构为数组加链表结构,dictEntry是一个key-value结点,数组每一项为一个dictEntry,链表是以数组中的dictEnrty为头,next为下一节点指针的单向列表。
dict的数组有两个位,扩容时ht[1]会作为扩容哈希表。
privdata放置的是函数需要的自定义参数,而dictType是类型特定函数,两者一起使用才可以完整调用函数。
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);
}
/*
* dict 字典
* 大家需要关注的是dictht ht[2]:
* 这里设计存储两个dictht 的指针是用于Redis的rehash,后文中进行详解
*/
typedef struct dict {
dictType *type; /*类型特定函数*/
void *privdata; /*私有数据*/
dictht ht[2]; /*用于存储数据的两个hash表,正常只有一个hash表中有数据,只有在rehash的过程中才会出现两个hash表同时存在数据*/
long rehashidx; /*rehash目前进度,当哈希表进行rehash的时候用到,其他情况下为-1*/
unsigned long iterators; /*迭代器数量*/
} dict;
/*
* 这是我们的哈希表结构。 每个字典都有两个
* 一个哈希表里面有多个哈希表节点(dictEntry),每个节点表示字典的一个键值对
*/
typedef struct dictht {
dictEntry **table; /*哈希表数组指针*/
unsigned long size; /*hashtable 容量 数组大小*/
unsigned long sizemask; /*size -1*/
unsigned long used; /*hashtable中元素个数,正常情况下当used/size=1时将进行扩容操作*/
} dictht;
/*
* 哈希表节点
*/
typedef struct dictEntry {
void *key;
union {
void *val; /*指向Value值的指针,正常是指向一个redisObject*/
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next; /*当出现hash冲突时使用链表形式保存hashcode相等但是field 不相等的数据,这里就是指向下一条数据的指针*/
} dictEntry;
hash算法
将key传入dict的第一个函数,把返回值和mask做与操作
公式为unsigned int (*hashFunction)(const void *key)&mask
mask值根据rehashidx是否为-1,决定使用表0的还是表1的
hashtable扩缩
扩缩条件
负载因子算法为used/dictEntry数组的大小,由于哈希冲突,此值可以大于1
太大会造成不必要的计算而浪费性能,太小会造成过多哈希冲突,要会浪费性能
bgsave和aof_rewrite使用了copy-on-write操作,仅使用内存来临时响应这段时间客户端的请求,对内存需求较大,所以在使用其中某个时,扩载因子要达到5,否则达到1就扩载。
如果负载因子小于0.1,开始收缩
渐进式扩缩
为了保证伸缩时正常使用,会采用渐进式。
先将rehashidx赋为0,
每次先操作0表,如果找到把该数据从0表移到1表(上面的hash算法),然后rehashidx+1,如果找不到会去1表找。
直到表1used为0,就将0指针指向1表,然后分配新的空间,将1指针指向新空间。将rehashidx赋为-1.
跳表
数据结构
zskiplist中,header是一个指向32层zskiplistNode的指针,这是因为跳表节点最高层即为32层,level是最高层数(不含头节点),length是跳表节点的个数(不含头节点),用于O(1)获得高度和个数。
zskiplistNode中,ele是节点值,robj指向一个保存SDS的对象。score是节点用来比较的大小的数值,可以理解为comparator的参数。backward用来指向上一个跳表节点,backward可以理解为跨度为1的指向上一个节点的指针。
zskiplistLevel是节点的层数,span是指两个节点之间的跨度,如图两个level[2]之间跨度为5,意味着经过了5个节点,跨度的作用是用于快速查找时也可以高效获得排位。
/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
robj* ele;
double score;
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;
遍历
首先进入头节点,比较最大层数的forward节点的值,如果大于,跳到该节点,如果小于,比较下一层。
如要获得score为3的点,已知最高层为2,所以比较2指向的forward点,发现score为5,就不跳,于是比较1指向的forward点,发现score为2,3大于等于2,那就跳,如此循环
(重复图片只为不用翻页)
插入(个人理论分析,理解即可)
虽然每个节点两两生成一个上层节点是最佳的logn查询结构,但是这种结构维护成本过大,每次都对其他节点指针甚至层数造成大批量改动,所以redis采用随机生成1-32的level数,然后进行score比较。
可以根据遍历的思想完成插入,如上图,首先判断节点和最大level的关系,节点level为1,而最大level为2,意味着我们不需要补充头节点到该节点的指针,进入第一个节点的level1,level1的forward指向节点3的score为2,所以直接跨过,第三个节点level1的forward指向节点5的score为5,所以不需要跨过,我们先把插入节点的level1的forward指向节点5,把节点5的backward指向新插入节点,然后让第三个节点forward指向新的节点的level1,把新插入节点的backward指向节点3,就完成了这一层的插入。
接下来我们可以直接从第三个节点的level0开始操作,而不是从第一个节点重新开始,循环上面步骤,将下面每一层都插好指针,最后确保zskiplistNode中level和length的更新。
ziplist
数据结构
很简单的结构,没什么好说的
注意点是两点。
1 prevrawlen在长度小于254时以一个字节呈现,长度大于等于254而变为5个字节,这意味着如果有多个250-253字节entry连续存在时,如果因为新添进来的entry大于等于254,会造成后续每个entry连环变化。新版本中使用listpack代替,它改为记录entry当前长度,从而避免这个问题。
2 ziplist中entry使用的content是一个字节数组,而不是StringObject(StringObject指使用数据类型模块中的String)。
intset
typedef struct intset {
// 编码方式
uint32_t encoding;
// 集合包含的元素数量
uint32_t length;
// 保存元素的数组
int8_t contents[];
} intset;
它可以保存类型为int16_t、int32_t或者int64_t的整数值,这由encoding进行标记,由插入的最大数据的大小决定,比如目前encoding为INTSET_ENC_INT16,那能插入最大数即为,65534(2的16次方位-1,由于0的存在少一个数,补码规定少去最大的正数),如果插入65535,那么就会改变encoding为INTSET_ENC_INT32,然后为每个数组元素重新分配空间。
SDS
SDS是redis自己实现的字符串
对于SDS仅需要知道以下几点:
1 由于len的存在,SDS不会因为\0而终止输入,这个特性让SDS拥有承装二进制流的特性,可以将对象序列化成二进制流存进去,拿出来再反序列化
2 SDS的len让它在伸缩时未必需要重新分配空间,也避免了溢出覆盖其它区域的问题
3 SDS是在C的String.h上改造的,所以还可以使用部分<String.h>的api
linkedlist
普通的双向链表
使用数据类型
String
int embstr raw
如果纯数字,那就是int
如果是浮点数或字符串,那就是embstr
如果大于等于32字节,那就是raw
整数技巧
从其他类型转回int类型前提:确实是整数
int通过append可以变为raw型,raw通过incrby变为int型
int通过incrfloatby可以变为embstr型,embstr通过incrby变为int型
list
ziplist->linkedlist
长度到达或单个过大会转变
set
intset->hashtable
长度到达或非纯整数会转变
zset
ziplist->skiplist
长度到达或单个过大会转变
hash
ziplist->hashtable
长度到达或单个过大会转变
两个可以尝试记忆的数值
512 这是hash,set和list的转变的长度,拿去蒙有3/4的概率对(不会有人拿去String用吧不会吧)
64 这是zset,hash和list的变化的最大大小(不会有人拿去String用吧不会吧,intset不限长度啊!)
来源
本人语雀笔记(语雀分享竟然收费了!失踪人口回归)