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

本文详细介绍了Redis中数据结构的实现,包括string、list、dict、skiplist、intset、ziplist、quicklist等,以及它们的编码格式如embstr、raw等。文章深入探讨了各数据结构的特性,如string的SDS实现、list的双向链表和快速列表quicklist、dict的哈希表和rehash策略等。同时,文章还提到了Redis的整体结构,包括每个数据库的redisDb结构体。
摘要由CSDN通过智能技术生成

数据结构实现

相信大家对 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)
  • 安全:记录了空闲空间,可以避免写缓冲区越界的问题
  • 内存友好:通过记录了空间信息,可以预分配空间,实现惰性删除,减少内存分配的同时不会造成内存泄露
  • 二进制安全:字符串内容可以为非 ASCII 编码,任意数据都能被编码为二进制字符串
  • 兼容 C 字符串:可以复用部分 C 标准库代码,避免无用重复

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)
  • 无环:没有设置哨兵节点,列表为空时,表头表尾均为 NULL
  • 多态:通过函数指针实现多态,数据结构可以复用

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索引:每执行一次 rehash 操作,dict.rehashidx计数器会加1当 rehash 完成后,dict.rehashidx会被设置为 -1
  • 触发条件
    计算当前负载因子:loader_factor = ht[0].used / ht[0].size
    收缩: 当 loader_factor < 0.1 时,执行 rehash 回收空闲空间
    扩展:没有执行 BGSAVE 或 BGREWRITEAOF 命令,loader_factor >= 1 执行 rehash正在执行 BGSAVE 或 BGREWRITEAOF 命令
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值