《Redis设计与实现》重点笔记①——基础数据结构

介绍

  • 文中数据和笔记参照《Redis设计与实现》, 书中内容详实仔细、引人入胜, 本文仅做笔记补充使用
  • 有些地方掺杂了个人理解和思考, 若有谬误,欢迎斧正

字符串

  • redis使用结构体SDS包装了c语言的字符串
    • 空间换时间, 降低获取长度的时间复杂度
    • 杜绝溢出(其实可以使用c函数strncat解决)
    • 二进制安全(因为使用len判断字符串长度, 字符串中的各种特殊字符不会阻碍读取)
    • 遵循C中使用’\0’作为字符串结束的标准, 可以重用String.h中的部分函数
struct sdshdr{
    int len; //字符串长度(忽略\0)
    int free; //数组剩余长度
    char buf[]; //实际存储字符串的数组
}

降低空间重分配次数策略

  • 空间预分配

    • 字符串实际长度 < 1MB时, buf = 2 * realLen +1(储存’\0’)
    • 字符串实际长度 >= 1MB时, buf = realLen + 1MB
  • 惰性空间释放

    • 缩短字符串长度时, 不会释放字符串数组的空间
    • 可通过api调用释放空间

链表

  • redis中广泛使用双端链表这一数据结构
  • 特点
    • 双端
    • 无环(头尾都指向NULL)
    • 有头尾指针
    • 有链表计数器
    • 多态(void* 指针可以适应各种情况)
// node的结构
typedef struct listNode{
    struct listNode *prev;
    struct listNode *next;
    void *value;
}listNode;
// list的结构
typedef struct list{
    listNode *head;
    listNode *tail;
    unsigned long len;
    void *(*dup)(void *ptr);  // 节点复制函数指针
    void *(*free)(void *ptr); // 节点释放函数指针
    int (*match)(void *ptr, void *key); // 节点对比函数指针
}list;

字典

字典的基本结构

  • 每个键独一无二的键值对集合
  • 使用hash表作为底层实现
// hash表节点
typedef struct dictEntry{
    void *key;
    union{ // 值是整数就直接保存, 否则就指针形式保存
        void *val;
        uint64_t u64;
        int64_t s64;
    } v;
    struct dictEntry *next; // 串联相同hash值的节点
}dictEntry;

// hash表
typedef struct dictht{
    dictEntry **table; // 一个指向dictEntry*类指针的指针, 作为指向dictEntry*指针数组的指针
    unsigned long size; // 表大小
    unsigned long sizemask; // 表大小掩码, 值为size-1
    unsigned long used; // 已使用多少节点
}
  • hash表示例

    • 在这里插入图片描述
  • 字典实现

typedef struct dictType{ // privdata是为了适应多态的可选额外信息
    unsigned int (*hashFunction)(const void *key); // 计算hash值函数指针
    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 *(*keyDestruction)(void *privdata, const void *key);  // 销毁键函数指针
    void *(*calDestruction)(void *privdata, const void *obj);  // 销毁值函数指针
} dictType;

typedef struct dict{
    dictType *type;  // 一组功能函数
    void *privdata;  // 可选参数
    dictht ht[2];    // 两个hash表, 正常适应ht[0], rehash时使用ht[1]
    int trehashidx;  // rehash次数, 没有rehash就是-1
} dict;

hash算法

  • 新生成的键值对K-V及其结构体dictEntry
  • 使用hashFunction计算出K的hash值 h
    • 使用MurmurHash作为Function
  • 将h与sizemask求与, h & sizemask
  • 根据结果将dictEntry挂到table的对应位置

rehash

  • 当table中k-v的数量到达标准后会进行rehash
    1. 扩展, 将ht[1]的大小设置为2n >= ht[0] * 2
    2. 收缩, 将ht[1]的大小设置为2n >= ht[0]
  • 将ht[0]上的键值对rehash到ht[1]上
  • 释放ht[0]
  • 将ht[1]和ht[0]交换
  • 在ht[1]创建一张新的空表

渐进式rehash

  • 当键值对数量过多时, 一次性rehash会导致服务器宕机
  • 操作
    • 先同时持有ht[0] 和 ht[1]
    • 在每一次对table中项进行增删改查的时候, 就将该项对应那一列项全部转移至ht[1]
    • 同时将rehashindex++
    • 直到全部转移后交换ht[0] 和 ht[1]
    • 并将rehashindex归为-1
  • tips
    • 删改查table时先搜索ht[0], 再搜索ht[1]
    • 新增操作全部用于ht[1]

rehash的时机

  • hashTable的size其实并不能实际限制表的存储数量, 更像是一种预期
  • load_factor(负载因子) = ht[0].used / ht[0].size
  • 扩展
    • 当服务器未执行BGSAVE或BGREWRITEAOF且load_factor > 1时扩展
    • 当服务器执行BGSAVE或BGREWRITEAOF且load_factor > 5时扩展
      • 尽可能减少执行这两个命令时的内存消耗
  • 收缩
    • load_factor < 0.1时收缩

跳跃表

  • 通过维护多个指针达到快速访问节点的目的(代替复杂的平衡树)
    • 时间复杂度: 平均O(logN), 最坏O(N)
  • 是redis中有序集合的实现之一
    • 有序集合中元素多、元素的成员是长字符串时使用跳跃表
    • 有序集合中每个元素有一个分值, 通过分值确定顺序
// 跳跃表节点
typedef struct zskiplistNode{
    // 层 -- 一组指向后不同步长的指针, 随机初始化层高在1 ~ 32之间(越大随机到的几率越小)
    struct zskiplistLevel{
        struct zskiplistNode *forward;
        unsigned int span;
    } level[];
    struct zskiplistNode *backward;
    double score;
    robj *obj;
} zskiplistNode;
// 跳跃表
typedef struct zskiplist{
    zskiplistNode *header, *tail; // 头尾指针
    unsigned long length; // 节点数
    int level; // 层数最大节点的层数
}
  • 跳跃表示意图

在这里插入图片描述

整数集合

  • 集合中元素数量较少且只包含整数时作为实现手段
typedef struct intset{
    uint32_t encoding; // 编码方式
    uint32_t length; // 集合的元素数量
    int8_t contents[]; //保存元素的数组, 从大到小排列
} intset;
  • contents中元素的数据类型不取决与int8_t, 而是取决于encoding的值

    • INTSET_ENC_INT16
    • INTSET_ENC_INT32
    • INTSET_ENC_INT64
  • 默认的encoding的类型是int16, 每次插入的数据规模大于现在的encoding类型时就会引发升级(不会降级)

    • 将encoding向上调一级
    • 扩大contents的容量使其匹配类型
    • 将之前的元素挪到正确的位置
    • 插入新元素(在头或者在尾)
  • 好处

    • 提升灵活性(可以添加任意类型的整数)
    • 节省内存(只在需要升级时升级)

压缩列表(ziplist)

  • 应用场景
    • 一个只包含小整数和较短字符串的列表(新版redis使用的是quicklist)
      • quicklist: 将多个ziplist做成双向链表
    • 一个键和值都是小整数或较短字符串的哈希表
  • 实现
    • 实质是linkedlist的一种
    • 每一个entry可以保存一个整数或者不定长的字符串(字符数组形式保存)
    • zltail是首节点到尾结点的偏移量(快速定位到最后一个节点)
表大小偏移量节点数节点实体表尾标记
zlbyteszltailzllenentry1zlend
  • entry中的content可以是长度分别为(26-1, 214-1, 232-1)的字节数组或六种不同长度整数的一种
前一节点长度content的长度实际保存的内容
previous_entry_lengthencodingcontent
  • previous_entry_length可以为1字节(前节点长度 < 254字节)或者5字节(首字节值为254)
    • 可以通过当前节点的位置 和 previous_entry_length算出前一节点的位置
  • 连锁更新(性能隐患)
    • 当有多个连续且大小处于250 ~ 253字节之间的节点时
    • 在这些节点前插入一个大小 > 254字节的节点, 会导致previous_entry_length长度 + 4
    • 从而引起一串的地址和空间重新分配问题
    • 还好这并不常见
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值