Redis 学习随笔(一)

199 阅读7分钟

一、Redis 数据结构与对象

        redis 有SDS、链表、字典、跳跃表、整数集合与压缩列表等数据结构。这些数据结构并不是我们平时操作 redis 所使用的,操作 redis 使用的是对象,包括字符串对象、列表对象、哈希对象、集合对象和有序集合对象。数据结构是这些对象的底层实现结构。例如: 哈希对象的底层实现可能是压缩列表或者是hashtable。

1、数据结构

(1)SDS:简单动态字符串。除了用于保存数据库中的字符串外,SDS还被用作缓冲区:AOF模块中的 AOF 缓冲区,以及客户端状态中的缓冲区。

  • SDS 的实现

    // redis 中的 SDS 由 sds.h/sdshdr 结构定义
    typedef struct sdshdr {
       int len;       // 已使用的空间
       int free;      // 未使用的空间
       char buf[];    // 字节数组,用于保存字符串,同 C 语言
    } sdshdr;
    
  • SDS 与 C 字符串相比的优点

    1、常数复杂度获取字符串的长度;
    2、杜绝缓冲区溢出;
    3、减少修改字符串时带来的内存充分配的次数;
    4、二进制安全
    

(2)链表:链表在 redis 中的使用十分广泛。比如列表键的底层实现之一就是链表。当列表中的元素比较多或者列表中的元素是比较长的字符串时,列表底层就会采用链表实现。除了列表键之外,发布与订阅、慢查询、监视器等功能也都用到了链表。redis 服务器使用链表保存多个客户端的状态信息,以及使用链表来构建客户端的输出缓冲区。

  • 链表的实现

    // redis 中的链表由 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;
    
    typedef struct listNode {
       struct listNode *prev;
       struct listNode *next;
       void *value;
    } listNode;
    

(3)字典:字典在 redis 中的使用十分广泛。redis 数据库的底层实现就是基于字典实现的。字典出了可以实现数据库,同时也是哈希键的底层实现之一。字典的底层采用哈希表实现。

  • 字典的实现

    // redis 中的字典由 dict.h/dict 结构定义
    typedef struct dict {
      dictType *type;          // 类型特定函数
      void *privdata;          // 私有数据
      dictht ht[2];            // 字典一般使用ht[0],只有对 ht[0] 进行 rehash 时,才会用到 ht[1]
      int rehashidx;          // 记录目前 rehash 的进度,当 rehash 不进行时,值为 -1;
    } dict;
    // redis 使用的哈希表由 dict.h/dictht 结构定义
    typedef struct dictht {
      dictEntry **table;       // 哈希表数据
      unsigned long size;      // 哈希表大小
      unsigned long sizemask;  // 掩码=size - 1,rehash 时用于计算负载因子
      unsigned long used;      // 哈希表已有节点的数量
    } dictht;
    // 哈希表节点结构
    typedef struct dictEntry {
      void *key;
      union {
        void *val;
        uint64_tu64;
        int64_ts64;
      }v;
      struct dictEntry *next;
    } dictEntry;
    
  • 哈希算法

    // 使用 hash 函数计算哈希值与索引值,一般 redis 采用 MurmurHash2 算法,
    该算法在输入有规律的情况下,仍能给出一个很好的随机分不行
    hash = dict->type->hashFunction(key);
    index = hash & dict->ht[x].sizemask;
    // 散列冲突时,redis 采用链地址法解决冲突。采用头插法,速度快。
    
  • rehash 扩容与收缩

    rehash 扩容的时机
     1)服务器目前没有执行 BGSAVE 或者 BGREWRITEAOF 命令,并且负载因子大于等于1;
     2)服务器目前正在执行 BGSAVE 或者 BGREWRITEAOF 命令,并且负载因子大于等于5;
    rehash 收缩时机
     1)负载因子小于等于 0.1
    负载因子计算:load_factor = ht[0].used / ht[0].size
    rehash 过程
     1)为哈希表 ht[1] 分配空间,空间大小取决于 rehash 的操作与 ht[0].used 的大小。如果
    是扩容操作,ht[1] 大小为第一个大于等于 ht[0].used * 2 的 2的 n 次幂。如果是收缩操作,ht[1]
    大小为第一个大于等于 ht[0].used 的2的 n 次幂。
     2)将 ht[0] 上的所有键 rehash 到 ht[1] 上,同时释放ht[0], 将 ht[1] 设置为 ht[0],重新为
    ht[1] 创建一个空的哈希表。
     rehash 的过程不是集中式的,是渐进式的。注意在 dict 结构体中有一成员 rehashidx,
    记录了当前rehash的进度,所以渐进式rehash的过程如下:
     1) 为 ht[1] 分配空间;
     2)将 rehashidx 设置为 0,表示 rehash 正式开始;
     3)每次对字典进行增删改查的操作时,除了执行本次操作,同时将 rehashidx 对应的所有键值对 rehash
    到 ht[1],当全部 rehash 后,将 rehashidx 置为 -1。
    

(4)跳跃表(skiplist):是一种有序数据结构,他通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问的目的。Redis 使用跳跃表作为有序集合的底层实现之一。如果一个有序集合包含的元素数量比较多,又或者有序集合中元素的成员是比较长的字符串时,Redis 就会使用跳跃表作为有序集合键的底层实现。除了用于实现有序集合,跳跃表还在集群节点中用作内部数据结构,除此之外,跳跃表在 redis 中再没有其他的用途。

// redis 中跳跃表由 redis.h/zskiplist 结构定义
typedef struct zskiplist {
   struct skiplistNode *head, *tail;  // 头尾指针
   unsigned int length;               // 表中节点的数量
   int level;                         // 表中层数最大的节点的层数
}
// 跳跃表节点的实现由 redis.h/zskoplistNode 定义
typedef struct zskiplistNode {
   struct zkskiplistLevel {
      struct zskiplistNode *forward;  // 前进指针
      unsigned int span;              // 跨度
   } level[];
   struct zskiplistNode *backward;    // 后退指针
   double score;                      // 分值
   robj *obj;                         // 成员对象
}
// 注:每个跳跃表的层高都是1 到 32 之间的随机数。

(5)整数集合:是集合键的底层实现之一。当一个集合只包含整数值元素,并且这个集合的元素数量不多时,redis 就会使用整数集合作为集合键的底层实现。

(6)压缩列表(ziplist):压缩列表是列表键和哈希键的底层实现之一。当一个列表键值包含少量的列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么 redis 就会使用压缩列表作为列表键的底层实现。

2、对象:Redis 并没有用之前的数据结构去实现键值对数据库,而是基于这些数据结构创建了一个对象系统。这个系统包括字符串对象、列表对象、哈希对象、集合对象和有序集合对象这五种类型的对象。

// redis 中对象结构由 redisObject 结构定义
typedef struct redisObject {
  unsigned type:4;    // 类型
  unsigned encoding:4 // 编码
  void *ptr;          // 指向底层实现数据结构的指针
  unsigned *lru:22;   // 空转时长
  ...
}
type 属性记录了对象的类型。键对象总是字符串对象,所以 type key,返回的值对象的类型。
encoding 属性记录了对象底层使用哪种数据结构作为对象的底层实现。可以使用命令 object encoding key
来进行查看。

(1)字符串对象:编码可以是 int、raw 或者 embstr。三者具体的区别可以参考 《redis设计与实现(第二版)》一书。

(2)列表对象:编码可以是 ziplist(压缩列表) 或者是 linkedlist(双端链表)。

  • ziplist 实现示意图

  • linkedlist 实现示意图

  • 编码转换:当列表对象可以同时满足以下两个条件时,列表对象使用 ziplist 编码。

    1、列表对象保存的所有字符串元素的长度都小于 64 字节;
    2、列表对象保存的元素数量小于512 个
    

(3)哈希对象:编码可以是 ziplist 或者是 hashtable。

  • ziplist 实现示意图
  • hashtable 实现示意图
  • 编码转换同列表对象

(4)集合对象:编码可以是 intset 或者是 hashtable;

  • intset 实现示意图

  • hashtable 实现示意图

  • 编码转换:满足以下两个条件使用 ziplist 编码

    1、集合保存的所有元素都是整数值;
    2、集合对象保存的元素数量不超过 512 个。
    

(5)有序集合对象:编码可以是 ziplist 或者是 skiplist(实际上是跳表 + 字典)。

  • ziplist 实现示意图

  • skiplist 实现示意图

  • 编码转换:满足以下两个条件使用 ziplist 编码。

    1、有序集合保存的元素数量小于 128 个。
    2、有序集合保存的所有元素成员的长度都小于 64 字节。
    以上两个条件的上限值可以修改,具体查看配置文件 zset-max-ziplist-entried 选项和 zset-max-ziplist-value 选项。