Redis为什么这么快之数据结构

  1. 前言
  2. String 类型的数据结构SDS
  3. 字典 Dic
  4. 整数集合 intset
  5. 压缩列表 ziplist
  6. 对象 object
  7. 总结
  8. 参考资料

前言

Redis 是使用 C语言编写的 key-value 数据库, 操作速度极快, 整体来说, 可以从数据结构和服务器事件驱动两个方面来解释. 下面就介绍redis所以引用的数据结构.

String 类型的数据结构SDS

Redis 没有使用 C语言传统的字符串, 而是构建了自定义的 SDS(simple dynamic string) 类型. Redis 使用C类型字符串的地方只是在一些字符串无需修改的地方, 如日志打印.
包含字符串的键值对在底层都是使用 SDS 实现的.

  1. SDS 的定义
    struct sdshdr{
     int len; // 记录buf数组已使用的字节的长度, 也是sds 字符串的长度.
     int free; // 记录buf数组中未使用字节的数量
     char buf[]; // 字节数组, 用于保存字符串
    }
    // SDS 实例 
    free = 0 // 表示该sds 没有分配任何使用空间
    len = 5 // 表示该sds 保存了一个5个字节长度的字符串
    buf = ['R','e','d','i','s','\0'] // char数组, 前五个字符为 'R','e','d','i','s';0
    // 最后一个字节则保存了空字符串 '\0'. 这样的好处是可以重用部分 C 函数.
  2. SDS 的字符串连接策略
  • 当修改后len 长度小于1MB时, free 分配len 长度相同的空间
    ```c
    free = 0 free = 10
    len = 5 + “Redis” => len = 10
    buf = [‘R’,’e’,’d’,’i’,’s’,’\0’]len=6 buf = [‘R’,’e’,’d’,’i’,’s’,’R’,’e’,’d’,’i’,’s’,’\0’,…]len=21(free + len + 1)
  • 当修改后len 长度大于1MB时, free 固定分配 1MB 的空间
    free = 0                            free = 1MB
    len = 0.9MB    + "长度1MB字符串" =>  len = 1.9MB
    buf = [...]len=2MB                  buf = [...]len=2.9MB + 1byte(free + len + 1byte)
    
3. SDS 的字符串移除策略, 修改free, len, buf 的值, 但是不回收空间, 避免再操作时重新分配内存的问题.
4. SDS 字符串二进制安全, 是因为它写入时, 直接将元数据写入到buf, 而不做任何限制过滤. 读取时, 是根据len来读取, 而不是和C语言类似的以 '\0'结尾, 遇到特殊字符就被截断的问题. 写入什么就读取什么.

相对C字符串的优点
- 常熟复杂度的获取字符串长度
- 杜绝缓冲区移除
- 减少修改字符串长度时所需的内存分配次数
- 二进制安全
- 兼容部分 C 字符串函数

## 链表 LinkedList 
List 使用LinkedList(链表)作为数据结构, 出了List之外, 发布订阅, 慢查询, 监视器 也用到了链表.
1. ListNode 定义
```c
typeof struct listNode{
    struct listNode *prev; // 前置节点
    struct listNode *next; // 后置节点
    void *value; // 节点的值
}
  1. LinkedList 的定义
    typeof struct list{
     listNode *head; // 表头 (首节点指向null, 不形成环形)
     listNode *tail; // 表尾 (尾节点指向null, 不形成环形)
     unsigned long len; // 链表长度
     void *(*dup)(void *ptr); // 复制节点所保存的值
     void (*free)(void *ptr); // 释放节点所保存的值
     int (*match)(void *ptr, void *key); // 用于对比节点所保存的值和另一个输入值是否相等.
    }
  2. 链表作为一个常见数据结构, 使用场景很多, 不用过多的介绍.

字典 Dic

字典是Redis 的String操作的底层实现

  1. 字典的底层实现则是 哈希表
    typeof struct dict{
     dictType *type; // 类型特定函数
     void *privatedata; // 私有数据, 保存哪些传递给特定函数的可选参数
     dictht ht[2]; // 哈希表, 字典只使用ht[0], 当进行rehash时使用 ht[1].
     int treshahidx; // rehash 索引, 当rehash 不再行进中时, 值为-1
    }
    

typeof struct dictht{
dictEntry **table;// 哈希表数组
unsigned long size; // 哈希表大小
unsigned long sizemask; // 哈希表大小掩码, 用于计算索引值. 总等于1
unsigned long used; // 该哈希表当前节点数量
}

typeof struct dictEntry{
void *key; // 键
union{ // 值
void *val;
uint64 _tu64;
int64 _ts64;
} v;
struct dictEntry *next; // 指向下一个哈希表节点, 形成环形
};

2. 计算hash的方法是 MurmurHash2 算法.
3. rehash时启用ht[1], 但是不是一次性完成, 而是渐进式完成.
4. 整体执行过程和 java中hashmap 类似. 这里不做过多的讲解.

## 跳跃表 skiplist
ZSET 在包含元素数量较多时就是使用 skiplist实现, 另一个地方就是集群节点的内部结构. 其效率可以和平衡树媲美, 且实现更简单.
插入和删除的时间复杂度就是查询元素插入位置的时间复杂度, 即 O(logN).

1. 结构定义
```c
typeof struct zskiplist{
    struct skiplistNode *head, *tail; // 表头节点和表尾节点
    unsigned long length; // 表中节点的数量
    int level; // 表中层数最大的节点的层数
}
typeof struct zskiplistNode{
    struct zskiplistLevel{ // 层
        struct zskiplistNode *forward; // 前进指针
        unsigned int span; // 跨度
    } level[];
    struct zskiplistNode *backward; // 后退指针 
    double score; // 分数
    robj *obj; // 成员对象
}
  1. 跳跃表的一些特点
  • Redis跳跃表的节点本身是一个链表节点
  • 跳跃表的每个节点包含一个数组类型的层字段, 层中的元素包含指向后序第n个节点的指针
  1. 跳跃表的插入操作
    // 插入第一个数 1 
    Level1:  head -> Node(1)
    // 插入第二个数 2. 1: 先插入节点到Level1, 然后根据随机算法生成level数, 假如level=2 
    Level2: head -> Node(1) -> Node(2)
    Level1: head -> Node(1) -> Node(2)
    // 插入第三个数 3. 1: 和插入第二个节点类似, 假如level=1
    Level2: head -> Node(1) -> Node(2)
    Level1: head -> Node(1) -> Node(2) -> Node(3)
    // 插入第四个数 4. 1: 和插入第二个节点类似, 假如level=1
    Level3: head -> Node(1)            ->            Node(4)
    Level2: head -> Node(1) -> Node(2)
    Level1: head -> Node(1) -> Node(2) -> Node(3) -> Node(4)
  2. 跳跃表的随机算法
    level = n 的概率就是 ZSKIPLIST_P ^ (n-1)
    int zslRandomLevel(void) {
     int level = 1;
     while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF)) // ZSKIPLIST_P = 0.25
         level += 1;
     return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL; // ZSKIPLIST_MAXLEVEL = 32
    }
  3. 跳跃表的查找
    1. 从最高层通过forward指针向前查找, 如果前一个元素大于待查找元素或者遇到tail指针, level -1 继续查找; 如果前一个元素小于带查找元素, 那么继续通过forward 向前查找.
    2. 重复步骤1, 直到找到节点或者返回null.

整数集合 intset

intset 是Set 的底层实现之一, 当一个集合只包含整数值元素且元素不多的时候启用.

  1. 结构定义

    typeof struct intset{
     // 编码方式: 1. INTSET_ENC_INT16 content中是int16_t的整数值
     // 2. INTSET_ENC_INT32 content中是int32_t的整数值
     // 2. INTSET_ENC_INT64 content中是int64_t的整数值
     uint32_t encoding; 
     uint32_t length;// 集合中的元素数量
     int8_t contents[]; // 元素数组
    } intset;
  2. 升级和降级
    当数组中存放的三个元素1,2,3时, encoding是 int16_t, 当插入65536之后, encoding就将变成 int32_t, 然后数组长度从 163 变成 324, 这个过程称之为升级.
    当数组中存放的四个元素1,2,3, 65536时, encoding是 int32_t, 当删除65536之后, encoding不改变, 然后数组长度从 324 变成 323, 这个过程称之为降级.

  3. 升级操作为整数集合带来了操作上的灵活性, 并且节约了内存; 整数集合只支持升级不支持降级.

压缩列表 ziplist

压缩列表是 list和hash 的底层实现之一. 当list 中只包含少量项并且 每个列表项都是小整数或较短的字符串, 就会使用压缩表. 主要是为了节约内存而开发的顺序型结构.

对象 object

  1. 当我们操作redis 时, 不是直接操作上面的数据结构, 而是操作对象结构, 并给对象设置所使用编码. 编码对应其真正使用的底层结构. 这样能提升 Redis 的灵活性和效率.
  • REIDS_STRING 字符串 编码类型: int , raw(SDS), embstr.
  • REDIS_LIST 列表 编码类型: ziplist, linkedlist.
  • REDIS_HASH 哈希表 编码类型: ziplist, hashtable
  • REDIS_SET 集合 编码类型: intset, hashtable
  • REDIS_ZSET 有序集合 编码类型: ziplist, skiplist
  1. 对象共享机制
    假如键A 创建了一个整型值 100, 在键B 创建另外一个 整型值100 的时候, 直接将键A 的值指向 键B, 更新了整型值的引用计数而已. 这样有效的节约了内存.
    共享机制只共享整数值(0-9999)的字符串, 不共享其他类型.
  2. 对象的引用计数为0 时, 自动释放内存.
  3. 对象会记录自己最后一次被访问时间, 这样当服务器的回收策略合适时, 会回收这些内存, 提高效率.

总结

Redis 数据结构高效的真正原因就是因为数据结构 对象的原因.

参考资料

Redis 设计与实现


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 zhao4xi@126.com

文章标题:Redis为什么这么快之数据结构

文章字数:2.2k

本文作者:Zhaoxi

发布时间:2018-12-31, 15:15:35

最后更新:2019-09-21, 15:16:03

原始链接:http://zhao4xi.github.io/2018/12/31/Redis为什么这么快之数据结构/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录