NoSql之Redis系列三: Redis为什么这么快之数据结构

NoSql之Redis系列三: Redis为什么这么快之数据结构

    • 前言
    • String 类型的数据结构SDS
    • 链表 LinkedList
    • 字典 Dic
    • 跳跃表 skiplist
    • 整数集合 intset
    • 压缩列表 ziplist
    • 对象 object
    • 总结
    • 参考资料

前言

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 函数.
  1. SDS 的字符串连接策略
  • 当修改后len 长度小于1MB时, free 分配len 长度相同的空间
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 的空间
```c
free = 0                            free = 1MB
len = 0.9MB    + "长度1MB字符串" =>  len = 1.9MB
buf = [...]len=2MB                  buf = [...]len=2.9MB + 1byte(free + len + 1byte)

  1. SDS 的字符串移除策略, 修改free, len, buf 的值, 但是不回收空间, 避免再操作时重新分配内存的问题.
  2. SDS 字符串二进制安全, 是因为它写入时, 直接将元数据写入到buf, 而不做任何限制过滤. 读取时, 是根据len来读取, 而不是和C语言类似的以 '\0’结尾, 遇到特殊字符就被截断的问题. 写入什么就读取什么.

相对C字符串的优点

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

链表 LinkedList

List 使用LinkedList(链表)作为数据结构, 出了List之外, 发布订阅, 慢查询, 监视器 也用到了链表.

  1. ListNode 定义
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); // 用于对比节点所保存的值和另一个输入值是否相等.
}
  1. 链表作为一个常见数据结构, 使用场景很多, 不用过多的介绍.

字典 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; // 指向下一个哈希表节点, 形成环形
};
  1. 计算hash的方法是 MurmurHash2 算法.
  2. rehash时启用ht[1], 但是不是一次性完成, 而是渐进式完成.
  3. 整体执行过程和 java中hashmap 类似. 这里不做过多的讲解.

跳跃表 skiplist

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

  1. 结构定义
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)
  1. 跳跃表的随机算法
    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
}
  1. 跳跃表的查找
    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;
  1. 升级和降级
    当数组中存放的三个元素1,2,3时, encoding是 int16_t, 当插入65536之后, encoding就将变成 int32_t, 然后数组长度从 163 变成 324, 这个过程称之为升级.
    当数组中存放的四个元素1,2,3, 65536时, encoding是 int32_t, 当删除65536之后, encoding不改变, 然后数组长度从 324 变成 323, 这个过程称之为降级.

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

压缩列表 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 设计与实现

你可能感兴趣的:(Redis)