数据结构

redis 的根对象是一个 redisDb 结构体。里面有个 dict 字段,存储了所有的 kv,还有个 expire 字段,存储了所有设置了过期时间的 key

typedef struct redisDb {
    dict *dict;          // 保存所有 kv
    dict *expires;       // 设置了过期时间的 key 及其过期时间
}

// src/dict.h
// 哈希表
typedef struct dict {
    dictht ht[2];   // ht=hash table,每个dict有俩,增量扩容时使用
} dict;

// 哈希表子结构
typedef struct dictht {
    // 哈希表数组
    // 可以看作是:一个哈希表数组,数组的每个项是entry链表的头结点(链地址法解决哈希冲突)
    dictEntry **table;
} dictht;

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

当我们使用 EXPIRE key duration 命令对一个 key 设置过期时间时,会将该 key 保存到 expires 这个字典中,就是说一个 key 存了两次,额外占用内存。不过这个 keyvalue 不是实际数据的 value,而是过期时间。

dict 的结构详见 哈希表结构arrow-up-right

dictEntry 里的 keyvalue,由一种叫做 redisObject 的结构来存储。

RedisObject

redis 命令中,对 key 进行处理的命令占了很大一部分。对于不同的 value 类型,key 能执行的命令又大不相同。比如说,LPUSHLLEN 只能用于 list 类型的 key。另外一些命令,比如 DELTTLTYPE,可以用于任何类型的 key,但底层的执行逻辑又是不一样的。说明 redis 必须让每个 key 都带有类型信息,使得程序可以检查 key 的类型,并为它选择合适的处理方式。

为了解决以上问题,redis 构建了自己的类型系统,用来储存 valueRedisObject 是其中的核心。其结构如下:

type, encoding, 和 ptr 是最重要的3个属性。

type 可能的值如下:

encoding 可能的值如下:

String

结构为 SDS,编码有如下三种:

  • int:这种编码支持 incrdecr 命令

  • embstr:长度小于 44 的字符串

  • raw:长度超过 44 的字符串

List

  • quicklist: linkedlistziplist 的结合。

Hash

  • ziplist: 数据量小时,会直接用 ziplist 编码

  • hashtable: 数据量大时,改为 hashtable 编码

Set

  • intset: 元素全为 int,且数量小于配置时,使用该特殊编码

  • hashtable: 其它情况使用 hashtable 编码。其实就是个 valueNULLHash 的结构

Sorted Set

  • ziplist: 数据量小时,使用该编码

  • skiplist + hashtable: 其余情况使用该编码,跳表用于排序,hashtable 用于 O(1) 查找元素

image-20211027194146066

高级数据结构

Bitmap

底层为 String,只不过操作的粒度变成了位。因为 String 类型最大长度为 512MB,所以 bitmap 最多可以存储 2^32bit

GEO

  • 本质上还是借助于 ZSET,并且使用 GeoHash 技术进行填充。Redis 中将经纬度使用 52 位整数进行编码,放进 zset 中,score 就是这 52 位整数值

  • 通过对 score 进行排序就可以得到坐标附近的其它元素,将 score 还原成坐标值就可以得到元素的原始坐标

HyperLogLog

  • 可以用来统计网站PV

  • 基本原理是伯努利实验,比如进行 n 轮抛硬币实验,每轮直到出现 1 为止,如果最多出现连续 20 然后才是 1 的情况 (001),那么 n 大概率为 8

    • 001 的概率为 1/2^3,也就是说如果 出现了 001 这个序列,说明起码抛了 8 次硬币。

  • 但是这样误差太大,因此 HyperLogLog 做了分桶。使用 PFAdd key offset 添加元素时,先计算元素的 64hash, 前 14 位用于分桶,后 50 位即伯努利过程,计算最长的连续 0 的个数。

  • 基于每个桶里 0 的最大长度计算平均值,并乘以修正因子,得到最后的计数。

  • 感觉桶里 0 的最大长度,其实类似布谷鸟过滤器里的指纹。

Stream

  • 用作消息队列。底层的数据结构是 radix tree(基数树)

命令的类型检查和多态

有了 redisObject 结构的存在,在执行处理数据类型的命令时,进行类型检查和对编码进行多态操作就简单得多了。

当执行一个处理数据类型的命令时,Redis 执行以下步骤:

  1. 根据给定 key, 在数据库字典中查找和它相对应的 redisObject,如果没找到, 就返回 NULL

  2. 检查 redisObjecttype 属性和执行命令所需的类型是否相符,如果不相符,返回类型错误

  3. 根据 redisObjectencoding 属性所指定的编码,选择合适的操作函数来处理底层的数据结构。返回数据结构的操作结果作为命令的返回值

参考

redis容量估算arrow-up-right

头条面试真题:请谈谈Redis 9种数据结构以及它们的内部编码实现arrow-up-right

Jackey - 走近源码:神奇的HyperLogLogarrow-up-right

Last updated