Redis 数据结构与对象编码 (Object Encoding)

数据结构实现

相信大家对 redis 的数据结构都比较熟悉:

  • string :字符串(可以表示字符串、整数、位图)
  • list :列表(可以表示线性表、栈、双端队列、阻塞队列)
  • hash :哈希表
  • set :集合
  • zset :有序集合

为了将性能优化到极致,redis 作者为每种数据结构提供了不同的实现方式,以适应特定应用场景。

以最常用的 string 为例,其底层实现就可以分为 3 种: int , embstr , raw

127.0.0.1:6379> SET counter 1
OK
127.0.0.1:6379> OBJECT ENCODING counter
"int"
127.0.0.1:6379> SET name "Tom"
OK
127.0.0.1:6379> OBJECT ENCODING name
"embstr"
127.0.0.1:6379> SETBIT bits 1 1
(integer) 0
127.0.0.1:6379> OBJECT ENCODING bits
"raw"

这些特定的底层实现在 redis 中被称为 编码 encoding ,下面逐一介绍这些编码实现。

string

redis 中所有的 key 都是字符串,这些字符串是通过一个名为 简单动态字符串 SDS 的数据结构实现的。

typedef char *sds; // SDS 字符串指针,指向 sdshdr.buf

    struct sdshdr? { // SDS header,[?] 可以为 8, 16, 32, 64
        uint?_t len;          // 已用空间,字符串的实际长度
        uint?_t alloc;        // 已分配空间,不包含'\0'
        unsigned char flags;  // 类型标记,指明了 len 与 alloc 的实际类型,可以通过 sds[-1] 获取
        char buf[];           // 字符数组,保存以'\0'结尾的字符串,与传统 C 语言中的字符串的表达方式保持一致
    };

内存布局如下:

+-------+---------+-----------+-------+
|  len  |  alloc  |   flags   |  buf  |
+-------+---------+-----------+-------+
                   ^--sds[-1]  ^--sds

相较于传统的 C 字符串,其优点如下:

O(1)

list

redis 中 list 的底层实现之一是双向链表,该结构支持顺序访问,并提供了高效的元素增删功能。

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

    typedef struct list {
        listNode *head; // 头节点
        listNode *tail; // 尾节点
        unsigned long len;     // 列表长度
        void *(*dup) (void *ptr); // 节点值复制函数
        void (*free) (void *ptr); // 节点值释放函数
        int (*match) (void *ptr); // 节点值比较函数
    } list;

这里使用了函数指针来实现动态绑定,根据 value 类型,指定不同 dup , free , match 的函数,实现多态。

该数据结构有以下特征:

O(1)
O(1)

dict

redis 中使用 dict 来保存键值对,其底层实现之一是哈希表。

typedef struct dictEntry {
        void* key;  // 键
        union {     // 值,可以为指针、有符号长整,无符号长整,双精度浮点
            void *val;
            uint64_t u64;
            int64_t s64;
            double d;
        } v;
        struct dictEntry *next;
    } dictEntry;

    typedef struct dictht {
        dictEntry **table;      // 哈希表数组,数组中的每个元素是一个单向链表
        unsigned long size;     // 哈希表数组大小
        unsigned long sizemask; // 哈希掩码,用于计算索引
        unsigned long used;     // 已有节点数量
    } dictht;

    typedef struct dictType {
        unsigned int (*hashFunction) (const void *key);             // 哈希函数,用于计算哈希值
        int (*keyCompare)(void *privdata, const void *key1, const void *key2); // 键比较函数
        void *(*keyDup)(void *privdata, const void *key);           // 键复制函数
        void *(*valDup)(void *privdata, const void *obj);           // 值复制函数
        void *(*keyDestructor)(void *privdata, const void *key);    // 键销毁函数
        void *(*valDestructor)(void *privdata, const void *obj);    // 值销毁函数
    } dictType;

    typedef struct dict {
        dictType *type;     // 类型函数,用于实现多态
        void *privdata;     // 私有数据,用于实现多态
        dictht ht[2];       // 哈希表,字典使用 ht[0] 作为哈希表,ht[1] 用于进行 rehash
        int rehashidx;      // rehash索引,当没有执行 rehash 时,其值为 -1
    } dict;

该数据结构有以下特征:

  • 哈希算法:使用 murmurhash2 作为哈希函数,时间复杂度为 O(1)

  • 冲突解决:使用链地址法解决冲突,新增元素会被放到表头,时间复杂度为 O(1)

  • 重新散列:每次 rehash 操作都会分成 3 步完成

    步骤1:为 dict.ht[1] 分配空间,其大小为 2 的 n 次方幂

    步骤2:将 dict.ht[0] 中的所有键值对 rehash 到 dict.ht[1] 上

    步骤3:释放 dict.ht[0] 的空间,用 dict.ht[1] 替换 dict.ht[0]

rehash 的一些细节

  • 分摊开销

    为了减少停顿, 步骤2 会分为多次渐进完成,将 rehash 键值对所需的计算工作,平均分摊到每个字典的增加、删除、查找、更新操作,期间会使用 dict.rehashidx 记录 dict.ht[0] 中已经完成 rehash 操作的 dictht.table 索引:

    dict.rehashidx
    dict.rehashidx
    
  • 触发条件

    计算当前负载因子: loader_factor = ht[0].used / ht[0].size 

    收缩:当 loader_factor < 0.1 时,执行 rehash 回收空闲空间

    扩展:

    1. 没有执行 BGSAVE 或 BGREWRITEAOF 命令,loader_factor >= 1 执行 rehash
    2. 正在执行 BGSAVE 或 BGREWRITEAOF 命令,loader_factor >= 5 执行 rehash

    大多操作系统都采用了 写时复制 copy-on-write 技术来优化子进程的效率:

    父子进程共享同一份数据,直到数据被修改时,才实际拷贝内存空间给子进程,保证数据隔离

    在执行 BGSAVE 或 BGREWRITEAOF 命令时,redis 会创建子进程,此时服务器会通过增加 loader_factor 的阈值,避免在子进程存在期间执行不必要的内存写操作,节约内存

<
  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值