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

130 阅读8分钟

简单动态字符串

  • 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