redis数据类型解析

底层实现结构

背景知识

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不限长度啊!)

来源

本人语雀笔记(语雀分享竟然收费了!失踪人口回归)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值