Redis五个基本类型的底层数据结构源码分析

String:

底层数据结构

SDS (simple dynamic string,简单动态字符串)

为什么不使用c语言的char*实现string?:

1.获取长度需要o(n)遍历至’/0’

2.字符串内不能含’/0’ ->不能存二进制文件

3.字符串拼接是直接在dest后面赋值src的内容,dest原内存不足可能会出现缓冲区溢出,导致程序退出

c语言的拼接函数:

char *strcat(char *dest, const char* src)

SDS数据结构:

struct __attribute__ ((__packed__)) hisdshdr8 {
    uint8_t len; /* used */
    uint8_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};

Len: 通过成员变量获取字符串长度 O(1),通过len与计数来判断是否完成遍历

Alloc:在修改字符串时,通过alloc-len判断当前空间是否满足需求,否则进行拓容(小于1M翻倍,大于则加1M),不仅避免缓冲区溢出的问题,还减少了内存的分配次数

flags:指定字符串类型,通过按长度需求,减少结构占用内存

Buf:底层字符数组,可以存二进制文件

编译器:对采用**__attribute__属性的结构体,能够跳去字节对齐**,按结构体实际占用空间分配内存,节省内存空间

List:

底层数据结构

linkedListzipListquickList

linkedList:

节点结构:


typedf struct listNode{
    struct listNode *prev;
    struct listNode *next;
    void *value;
}listNode;

头结点结构:


typedf struct list{
    listNode *head;
    listNode *tail;
    //节点拷贝函数
    void *(*dup)(void *ptr);
    //释放节点函数
    void *(*free)(void *ptr);
    //判断两个节点是否相等的函数
    int (*match)(void *ptr,void *key);
    //链表长度
    unsigned long len;
}

缺陷:

节点之间的内存大部分都不是连续的,不能利用好CPU以页载入的缓存

就算list只有一个节点,也需要进行链表头节点的结构体分配,链表在数量不多时造成内存浪费

zipList:

由连续内存块组成的顺序数据结构,类似数组

优点:连续分配,内存使用效率高(指减少了内存碎片以及节点分配带来的内存消耗)

数据结构:

 typedf struct ziplist<T>{
    //压缩列表占用字节数
    int32 zlbytes;
    //尾节点离起始位置的偏移量,用于快速定位尾节点
    int32 zltail_offset;
    //元素个数
    int16 zllength;
    //元素内容
    T[] entries;
    //结束位 0xFF
    int8 zlend;
}ziplist

对队头和队尾的查询为O(1),其他为O(n), 支持双向遍历,但是不适合保存太多元素

节点:


typede struct entry{
    //前一个entry的长度
    int<var> prelen;
    //元素类型编码
    int<var> encoding;
    //元素内容
    optional byte[] content;
}entry

更新节点可能会出现 连锁更新

即:由于ziplist的内存大小是连续分配的,当节点原分配的空间大小不足以满足修改后的节点,需要给该节点重新分配内存,后面节点的空间也都要重新分配

  • linkedList与zipList 对比:

节点数量少时,使用zipList的存储效率更高

节点数量大时,linkedList的维护效率更高

quickList:

是zipList和linkedList的组合,通过linkedList分段,zipList存储数据,可选配LZF压缩算法进行数据压缩

节点结构:


typedf struct quicklistNode{
    quicklistNode* prev;
    quicklistNode* next;
    //压缩列表
    ziplist* zl;	
    //ziplist大小
    int32 size;		
    //ziplist 中元素数量
    int16 count;
    //编码形式 存储 ziplist 还是进行 LZF 压缩储存的zipList
    int2 encoding;			
    ...
}quickListNode

头结点结构:


typedf struct quicklist{
    //指向头结点
    quicklistNode* head;
    //指向尾节点
    quicklistNode* tail;
    //元素总数
    long count;
    //quicklistNode节点的个数
    int nodes;	
    //压缩算法深度
    int compressDepth;		
    ...
}quickList

redis.conf中定义默认ziplist长度为8k字节,也可以设置数量来限制单个ziplist最多n个节点

HashTable:

底层数据结构:

 // 双哈希表,用于拓容时渐进式rehash
    typedef struct dict {
        // ...
        // 两个哈希表,交替使用,用于 rehash 操作
        dictht ht[2]; 
        // 哈希表rehash进度条
        // -1 表示没有进行 rehash
        long rehashidx; 
        //...
    } dict;



    typedef struct dictht {
        // 数组的首地址,哈希表是通过数组实现的
        // 而数组中的每个元素都是一个 dictEntry *
        // 所以这里 table 的类型是 dictEntry **
        dictEntry **table;
        // 哈希表大小
        unsigned long size;  
        // 哈希表大小掩码,用于计算索引值
        unsigned long sizemask;
        // 该哈希表已有的节点数量
        unsigned long used;
    } dictht;



    typedef struct dictEntry {
        // 指向键值对中的键
        void *key;
        // 指向键值对中的值
        union {
            void *val;
            uint64_t u64;
            int64_t s64;
            double d;
        } v;
        // 指向下一个哈希表节点,形成链表
        struct dictEntry *next;
    } dictEntry;
  • 通过union结构,实现动态根据val的值来选择存储数据类型,当为结构体时则使用指针,浮点数用double… 来减少多余的空间浪费,节省内存
  • 哈希表通过(哈希值%哈希表大小(哈希桶数量))获得对应元素的存储位置(底层数组)

哈希冲突

指两个及以上的key被分配到了同一个桶上

Redis通过链式哈希(拉链法)解决哈希冲突。

局限性:随着链表长度增加,查询耗时也会增加(极端下O(N))

解决办法:rehash,对哈希表进行调整,并重新计算各个key的hash并分配位置

Rehash:

步骤:

  1. 给哈希表2分配大于哈希表1 两倍的2次幂空间
  2. 将哈希表1的数据复制到哈希表2
  3. 释放哈希表1的空间,将哈希表2设置为哈希表1
  4. 新建个哈希表作为哈希表2

在数据迁移期间,如果数据量过大,会阻塞redis-server服务

Redis使用渐进式rehash解决

渐进Rehash:

步骤:

  1. 给哈希表2分配大于哈希表1两倍的2次幂空间,初始化rehashidx为0
  2. 每次对哈希表进行操作(crud)时,redis额外将哈希表1对应rehashidx的桶复制到哈希表2,在哈希表1删除对应桶,rehashidx++;
  3. 反复操作直至rehashidx>哈希表1桶数量

注:查询/删除/更新时,在哈希表1,2中查,新增时只在哈希表2新增 保证哈希表1只删不增

Rehash条件:

  • ht[0] 的大小为 0;
  • ht[0].used > ht[0].size,即哈希表内的元素数量已经超过哈希表的大小,并且可以扩容;
  • ht[0].used 大于等于 ht[0].size 的 5 倍

Set:

底层数据结构:**

数据量不大,且value类型为整型:inset

其他情况:dict(hash)

inset:

 typedf struct inset{

    // 编码方式有三种 
    // 默认 INSET_ENC_INT16
    uint32_t encoding;
    // 集合元素个数
    uint32_t length;
    // 实际存储元素的数组 
    int8_t contents[];
                      
}inset;

# 16位,2个字节,表示范围 -2^16 ~ 2^16
# define INTSET_ENC_INT16 (sizeof(int16_t))   
# 32位,4个字节,表示范围 -2^32 ~ 2^32
# define INTSET_ENC_INT32 (sizeof(int32_t))   
# 64位,8个字节,表示范围 -2^64 ~ 2^64
#define INTSET_ENC_INT64 (sizeof(int64_t))   

升级:

指出现元素大小超过当前编码方式时,改变编码方式及执行相关操作的过程

升级过程:

  1. 计算当前已有元素内存占用大小 len*encoding
  2. 选择支持当前最大元素大小的新编码方式
  3. 计算转换成新编码所需新增的内存大小,尾插
  4. 从插入新数据的起始idx开始,将之前数据按新编码方式往前进行插入

优点:能够节约内存,只在必要时扩容

缺点:不支持降级

Zset:

底层数据结构:

zipList、skipList(dict(哈希表的字典)、zskipList)

  • 当元素大小<128&&member长度<64B时,使用zipList

  • 其他情况skipList一把梭

skipList:

//链表节点
typedef struct zskiplistNode {
    robj *obj;//元素指针
    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;


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值