Redis设计与实现 读书笔记(一)数据结构与对象

本文详细介绍了Redis中的一些核心数据结构,包括简单动态字符串(SDS)、embstr编码、链表、字典、跳跃表、整数集合、压缩列表及其在Redis中的应用和优化。SDS提供了预分配空间、防止缓冲区溢出等优点,embstr是针对短字符串的优化。字典使用哈希表实现,支持多态和渐进式rehash。跳跃表用于有序集合和集群节点数据结构,提供高效查找。整数集合在集合键和小整数场景下使用,压缩列表则用于存储少量数据。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

简单动态字符串

  • background

    • SDS,simple dynamic string
    • SDS是Redis的默认字符串表示
    • 还可以作为缓冲区,AOF buffer,输入缓冲区
    • C字符串只会用作字符串字面量用在无需对字符串值进行修改的地方
  • 实现

    struct sdshdr{
    int len;
    int free;
    char buf[];
    }

  • 与C字符串的区别

    • 获取长度 O(1)

    • 杜绝缓冲区溢出

      • C串的strcat要求desc后面为src预留了足够的memory,否则缓冲区溢出
      • SDS的sdscat会先检查free,不满足的话拓展,然后再实际的修改操作
    • 减少内存重分配次数

      • 通过未使用空间实现了空间预分配和惰性空间释放

      • 空间预分配

        • 进行修改之后,len < 1 MB,分配使得 free = len
        • 进行修改之后,len >= 1 MB,分配 free = 1 MB
      • 惰性空间释放

        • 并不立即使用内存重分配来回收缩短后多出来的字节,而是用free熟悉记录下来
        • SDS提供API真正释放未使用空间
    • 二进制安全

      • C串只能保存文本,不能保存二进制数据,因为其以\0判断结尾
      • SDS的 buf 称为字节数组,用len判断结尾
      • redis用SDS不是保存字符,而是保存二进制数据
      • 既可以保存文本,又可以保存二进制数据
    • 兼容部分C字符串函数

      • 使用\0结尾以适配

embstr

  • 专门用来保存短字符串的一种优化编码方式

  • embstr编码与raw编码对应的字符串对象,都是由对象结构(redisObject)和数据结构(sdshdr)组成的

  • 执行命令时产生的效果是相同的的

  • 两者的区别

    • 分配/释放 空间的两次 / 一次

      用raw编码的字符串对象会调用两次内存分配函数来分别创建redisObject结构和sdshdr结构,而embstr编码则通过调用一次内存分配函数来分配一块连续的空间,空间中一次包含redisObject和sdshr两个结构

    • 空间的 不连续 / 连续

      embstr编码的对象比raw编码的对象能够更好的利用缓存带来的优势

链表

  • 支持的功能

    • 列表键,发布与订阅,慢查询,监视器,保存多个客户端的状态信息,构建客户端输出缓冲区
  • 实现

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

    typedef struct list{
    listNode *head;
    listNode *tail;
    unsigned int len;
    void *(*dup)(void *ptr);
    void *(*free)(void *ptr);
    void *(*match)(void *ptr, void *key);
    }list;

  • 特性

    • 双向、无环

    • 带表头和表尾

    • 带长度计数器

    • 多态

      • 通过为list设置类型特定的函数,可以使用list保存各种不同类型的值

字典

  • 支持的功能

    • Redis数据库,哈希键
  • 底层实现

    • 字典的底层实现是哈希表
    • 一个哈希表有多个哈希表节点,每个哈希表节点保存来字典中的一个键值对
  • 实现

    • 哈希表

      dict.h/dictht
      typedef struct dictht{
      // 哈希表数组
      dicEntry **table;
      // 哈希表数组的大小
      unsigned long size;
      // 掩码,用于计算数组索引,= size - 1
      unsigned long sizemask;
      // 已有节点的数量
      unsigned long used;
      }dictht;

    • 哈希表节点

      typedef struct dictEntry{
      // 键
      void *key;
      // 值
      union{
      void *val;
      uint64_tu64;
      int64_ts64;
      }v;
      // 形成链表
      struct dictEntry *next;
      }dictEntry;

    • 字典

      typedef struct dict{
      // 类型特定函数
      dictType *type;
      // 私有数据
      void *privdata;
      // 哈希表
      dictht ht[2];
      // rehash索引
      int trehashinx;
      }dict;

      • type和privdata是针对不同类型的键值对创建多态字典而设置的

      • type是dictType类型的指针,dictType保存了一系列用于操作特定类型键值对的函数

      • privdata保存了type的可选参数

  • 哈希算法

    • hash = dict->type->hashFunction(key);
    • index = hash & dict->ht[x].sizemask;
    • 使用链地址法解决键冲突,总是将新节点添加到表头
  • rehash

    • 为ht[1]分配空间

      • 拓展

        • 新大小 = 大于等于ht[0].used * 2的最小的2的n次方
      • 收缩

        • 新大小 = 大于等于ht[0].used的最小的2的n次方
    • 将ht[0]中所有键值对rehash到ht[1]上

      • rehash = 重新计算哈希值和索引值
    • ht[1]设置为ht[0],ht[1]创建一个空白哈希表

  • 拓展与收缩

    • 负载因子 load factor

      • used / size
    • 拓展

      • 取决于Redis服务器是否在BGSAVE或BGREWRITEAOF
      • 分界线为 1 和 5
      • 在上述两命令执行过程中,Redis需要创建子进程,OS采用写时复制,避免不必要的内存写入,节约内存
    • 收缩

      • 分界线为0.1
  • 渐进式rehash

    • 流程

      • 分多次,渐进式地慢慢地rehash
      • 为ht[1]分配空间,同时持有ht[0]和ht[1]
      • 维护索引计数变量rehashidx,置0,表示rehash正式开始
      • rehash期间,对字典对每次增删改查都会使ht[0]在rehashidx所有键值对rehash到ht[1],rehashidx++
      • 随着字典操作的不断进行,所有键值对都会被rehash,rehashidx=-1,rehash操作完成
    • 细节

      • delete,find,update会在两个表上同时进行
      • find,先在ht[0]上找,找不到再在ht[1]上找
      • add只会在ht[1]上进行,ht[0]键值对只增不减

跳跃表

  • backgound

    • Redis只在两个地方用到了跳表:有序集合键,集群节点的内部数据结构
  • 定义

    • zskiplistNode

      typedef struct zskiplistNode{
      // 层
      struct zskiplistLevel{
      // 前向指针
      struct zskiplistNode *forward;
      // 跨度
      unsigned int span;
      }level[];
      // 反向指针
      struct zskiplistNode *backward;
      // 分值
      double score;
      // 成员对象
      robj *obj;
      }zskiplistNode;

      • level

        • forward

          • 访问位于表尾方向的其他节点
        • span

          • 跨度
      • backward

        • 后退指针,从表尾向表头遍历时使用
      • score

        • double类型,分值
      • obj

        • 节点所保存的成员对象
    • zskiplist

      typedef struct zskiplist{
      // 表头和表尾
      struct skiplistNode *header, *tail;
      // 表中节点的数量
      unsigned long length;
      // 最大层数
      int level;
      }zskiplist;

      • header

      • tail

      • level

        • 跳跃表内除了表头外最大的层数
      • length

        • 跳跃表除了表头包含节点的数量
    • 细节

      • 表头

        • 计算length和level时不含
        • 结构和其他节点一样,但是很多字段不会被用到
      • 层中forward=null,则span=0

      • forward用于遍历,span用于计算排位

      • score相同时,按成员对象的字典序来排

      • 各个节点保存的成员对象必须是唯一的

整数集合

  • backgroud

    • intset
    • 集合键的实现之一
    • 一个集合只含有整数且数量不多时,Redis使用整数集合实现
  • 实现

    typedef struct intset{
    // 编码方式
    uint32_t encoding;
    // 集合包含的元素数量
    uint32_t length;
    // 保存元素的数组
    int8_t contents[];
    }intset;

    • encoding

      • contents真正的类型取决于encoding而不是int8_t
    • length

      • 集合元素的数量 = contents数组的长度
    • contents[]

      • 整数集合的每个元素都是content数组的一个数据项,数组中不包含任何相同的数据项
  • 升级

    • 何时升级:新元素添加到整数集合且类型比现有类型长

    • 怎么升级:

      • 扩容底层数组
      • 从尾往头复制原有数组,注意有序性不变
      • 添加新元素到底层数组的头或尾(想想为什么)
    • 好处

      • 提升灵活性

        • 保存int16_t / int32_t / int64_t 到集合而不担心类型错误
      • 节约内存

        • 有需要时才升级
    • 不能降级

压缩列表

  • background

    • ziplist
    • 集合键和哈希键的底层实现
    • 列表键含少量列表项,小整数 / 短字符串时,Redis使用压缩列表
    • 哈希键少量键值对,键和值都是小整数 / 短字符串时
  • 压缩列表的构成

    • 连续内存块组成短sequential数据结构
    • 一个压缩列表含任意多个节点entry
    • 每个entry保存一个字节数组或一个整数值
    • zlbytes | zltail | zllen | entry1 | entry2 | entry3 | ****** | entryN | zlend
    • zlbytes:整个压缩列表占的字节数,内存重分配或算zlend时使用
    • zltail:zlbytes起始到表尾节点起始的字节数
    • zllen:包含的节点数,值大于UINT16_MAX时需遍历列表才能得出节点数
    • entryX:各个节点,长度不定
    • zlend:标记压缩列表的末端
  • 压缩列表节点的构成

    • previous_entry_length | encoding | content

    • previous_entry_length:压缩列表前一个节点的长度

    • encoding:记录content属性所保存数据的类型和长度

    • content:节点的值,可以是一个字节数组或整数

      • 三种长度的字节数组 / 六种长度的整数值
  • 连续更新

对象

s

  • Redis对象系统

    • 字符串对象
    • 列表对象
    • 哈希对象
    • 集合对象
    • 有序集合对象
  • 定义

    typedef struct redisObject{
    // 类型
    unsigned type;
    // 编码
    unsigned encoding;
    // 指向底层数据结构的指针
    void *ptr;
    // 引用计数
    int refcount;
    // 最后一次被访问的时间
    unsigned lru;
    }robj;

    • type

      • 上述五个中一个
    • encoding

      • 对象所使用的编码 = 用了什么底层数据结构作为实现?
    • ptr

      • 指向底层数据结构的指针
    • refcount

      • 引用计数,用于垃圾回收,为0时释放内存
    • lru

      • 对象最后一次被命令程序访问的时间
  • 字符串对象

    • 编码类型:int,raw,embstr

    • int

      • 保存的是整数,整数值可以用long表示,void*转化为long来表示
    • raw

      • 保存的是字符串值,长度>32字节,使用SDS编码
    • embstr

      • 保存的是字符串值,长度 <= 32字节,使用embstr编码
    • 存储能用long double表示的浮点数时会先将浮点数转换为字符串,遵循上述长度规则,执行特定操作时再转换回浮点型

    • 编码的转换

      • 对象保存的不再是整数值,而是一个字符串值

        • int转换为raw
      • 存储的整数值超过long的范围

        • int转换为embstr
      • embstr编码的字符串被修改

        只有int、raw编码的字符串对象可以被修改,所以embstr编码的字符串实际上是只读的

        • embstr转换为raw

XMind - Trial Version

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值