Redis读书笔记(一)

Redis数据结构

1 简单动态字符串

Simple dynamic string 的实现

// sds.h/sdshdr
struct sdshdr {
  int len;    //记录buf数组中已使用的字节数, 不包括结尾空字符'\0'
  
  int free;   //记录buf数组中未使用的字节数
  
  char buf[];    //字节数组, 保存字符串
};

简单动态字符串SDS与C字符串的区别

  • 获取字符串长度的时间复杂度
    • C字符串: O(N), 需要遍历字符串
    • SDS: O(1)
  • 缓存区溢出问题
    • C字符串修改时需要为目标字符串分配足够的内存,否则会产生缓存区溢出
    • SDS需要修改时,会检查SDS的 free 空间是否满足要求,如果不满足会自动扩展空间
  • 减少内存重分配次数
    • C字符串每修改一次都需要重新分配内存
    • SDS通过空间预分配和惰性空间释放优化内存分配次数

空间预分配

  • 如果SDS修改之后的长度(len)小于1MB,那么程序分配同样大小的未使用空间(free)
  • 如果SDS修改之后的长度(len)大于等于1MB,那么程序会分配1MB的未使用空间(free)

惰性空间释放

  • 当SDS缩短时,程序并不会立即使用内存重分配回收缩短的字节,而是使用 free 属性将这些字节的数量记录
  • 需要时可以使用sdsfree释放未使用空间,所以不会造成内存浪费

2 链表

应用场景:列表键(List)的底层实现之一、发布与订阅、慢查询、监视器等。

链表的实现

// adlist.h/listNode
typedef struct listNode {
  struct listNode *prev;    //前置节点
  struct listNode *next;    //后置节点
  void *value;    //节点的值
} listNode;

// adlist.h/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;

Redis链表特点

  • 双端链表
  • 无环:头节点的prev和尾节点的next指针都指向NULL
  • 链表节点使用 void* 保存节点值,所以链表可以保存不同类型的值

3 字典

应用场景:哈希键(Hash)的底层实现之一,Redis数据库。

字典的实现

//哈希表
typedef struct dictht {
  dictEntry **table;        //哈希表数组
  unsigned long size;       //哈希表大小
  unsigned long sizemask;   //总等于size-1, 用于计算索引值
  unsigned long used;       //已有节点数量
} dictht;

//哈希表节点
typedef struct dictEntry {
  // 键
  void *key;
  
  //值
  union{
    void *val;
    uint64_t u64;
    int64_t s64;
  } v;
  
  struct dictEntry *next;    //指向下一个哈希表节点,形成链表
} dictEntry;

//dictType
typedef struct dictType {
  //计算哈希值
  unsigned int *hashFunction(const void *key);
  
  //复制键的函数
  void **keyDup(void *privdata, const void *key);
  
  //...
  
} dictType;

//Redis字典结构
typedef struct dict {
  dictType *type;    //保存了一簇用于操作特定类型键值对的函数
  
  void *privdata;    //需要传给特定类型函数的可选参数
  
  dictht ht[2];      //哈希表
  
  int rehashidx;     //rehash索引
}

哈希冲突

当有两个或以上数量的键被分配到哈希表数组的同一个索引上面时,则称发生了冲突
Redis的哈希表使用链地址法解决键冲突,每个哈希表节点都有一个next指针,多个被分配到同一索引的节点形成一个单向链表。出于速度考虑,程序总数将新节点添加到链表表头的位置,时间复杂度O(1)。

Rehash重新散列

为了让哈希表的负载因子维持在一个合理的范围内,当哈希表保存的键值对数量太多或者太少时,程序需要对哈希表的大小进行相应的扩展或者收缩(通过rehash操作实现)。

\[load\_factor = \frac{ht[0].used}{ht[0].size} \]

Rehash操作步骤

  1. 为字典ht[1]哈希表分配空间,大小取决于要执行的操作和ht[0].used属性值。

    • 如果是扩展操作,ht[1]的大小为第一个大于等于ht[0].used*2的\(2^n\) (2的n次方幂)
    • 如果是收缩操作,ht[1]的大小为第一个大于等于ht[0].used的\(2^n\)
  2. 将保存在ht[0]的所有键值对rehash到ht[1]上面:重新计算key的哈希值和索引值,然后将键值对放到ht[1]对应的位置上(这个过程是渐进式的,不是一步完成)。

  3. 当ht[0]包含的所有键值对都迁移到ht[1]后(ht[0]变为空表),释放ht[0],将ht[1]设置为ht[0],并新创建一个空白哈希表,为下一次rehash做准备。

渐进式Rehash期间的哈希表操作

  • 删除(delete)、查找(find)、更新(update)等操作会在两个哈希表上进行。
  • 新增(add)只会在ht[1]哈希表上进行,ht[0]不再进行任何添加操作。

4 跳跃表

应用场景:有序集合键(zset)底层实现之一、集群节点用作内部数据结构。

跳跃表的实现

//跳跃表节点
typedef struct zskiplistNode {
  //层,每个层带有两个属性:前进指针和跨度
  struct zskiplistLevel {
    struct zskiplistNode *forward;
    unsigned int span;
  } level[];
  
  //后退指针
  struct zskiplistNode *backward;
  
  //分值,节点按分值从小到达排列
  double score;
  
  //成员对象
  robj *pbj;
} zskiplistNode;

//跳跃表
typedef struct zskiplist {
  /**
   * header、tail: 头尾节点
   * length: 表中节点的数量(不包括头节点)
   * level: 层数最大节点的层数(不包括头节点)
   */
  struct zskiplistNode *header, *tail;
  unsigned long length;
  int level;
}

特性:跳跃表中的节点按照score大小进行排序,当score相同时,节点按照成员对象的大小进行排序。

5 整数集合

应用场景:集合键的底层实现之一(Set)。

  • 当一个集合只包含整数值元素,且这个集合元素数量不多时,Redis会使用整数集合作为集合键的底层实现。

整数集合的实现

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

整数集合特性

  • contents数组的真正类型取决于encoding属性值。如果encoding属性的值为INTSET_ENC_INT16,则contents就是一个int16_t类型的数组;如果encoding属性的值为INTSET_ENC_INT32,则contents就是一个int32_t类型的数组等待。
  • 当新元素添加到整数集合时,并且新元素的类型比整数集合现有元素都要长,整数集合需要先升级upgrade。

整数集合升级步骤

  1. 根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间
  2. 将底层数组现有的所有元素都转换成与新元素相同的类型,并将类型转换后的元素放到正确的位置上
  3. 将新元素添加至数组

6 压缩列表

应用场景:列表键(List)和哈希键(Hash)的底层实现之一。

  • 当一个列表键只包含少量列表项,并且每个列表项要么是小整数值,要么就是短字符串,那么Redis会使用ziplist来做列表键的底层实现。
  • 当一个哈希键只包含少量键值对,并且每个键值对的key和value都是小整数值或短字符串,那么Redis会使用ziplist来做哈希键的底层实现。

压缩列表的构成

属性 类型 长度 用途
zlbytes uint32_t 4字节 记录整个压缩列表占用的内存字节数。
zltail uint32_t 4字节 记录压缩列表表尾节点距离起始地址有多少字节。
zllen uint16_t 2字节 记录整个压缩列表包含的节点数量:
当属性值小于65535时,表示的就是整个压缩列表节点的数量;
当属性值等于65535时,节点的真实数量需要遍历才能得到。
entry1 列表节点 不定 压缩列表包含的各个节点,节点的长度由保存的内容决定。
entry2
...
entryN
zlend uint8_t 1字节 特殊值0xFF,用于标记压缩列表的末端。

压缩列表节点Entry的构成

属性 长度 用途
previous_entry_length 1字节或5字节 记录了压缩列表前一个节点的长度。
encoding 1字节、2字节或5字节 记录了当前节点的content属性所保存数据的类型以及长度。
content 由encoding决定 保存节点的值,可以是字节数组或者整数。

压缩列表特性

  • 添加新节点或者删除旧节点,可能会引发连锁更新操作,但是出现的几率非常低。
posted @ 2022-06-26 21:59  ylyzty  阅读(19)  评论(0编辑  收藏  举报