开发技巧Java学习笔记Java 杂谈

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

2020-10-31  本文已影响0人  并发量就是我的发量

数据结构实现

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

为了将性能优化到极致,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 字符串,其优点如下:

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 的函数,实现多态。

该数据结构有以下特征:

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;

该数据结构有以下特征:

rehash 的一些细节

skiplist

跳表是一种有序数据结构,并且通过维持多层级指针来达到快速访问的目的,是典型的空间换时间策略。
其查找效率与平衡树相近,但是维护成本更低,且实现简单。

    typedef struct zskiplistNode {
        sds ele;                        // 成员对象
        double score;                   // 分值
        struct zskiplistNode *backward; // 后退指针
        struct zskiplistLevel {
            struct zskiplistNode *forward;  // 前进指针
            unsigned long span;             // 跨度,当前节点和前进节点之间的距离
        } level[];
    } zskiplistNode;

    typedef struct zskiplist {
        struct zskiplistNode *header, *tail;// 头尾指针
        unsigned long length;               // 长度
        int level;                          // 最大层级
    } zskiplist;

该数据结构有以下特征:

intset

有序整型集合,具有紧凑的存储空间,添加操作的时间复杂度为O(N)。

    typedef struct intset {
        uint32_t encoding;  // 编码方式,指示元素的实际类型
        uint32_t length;    // 元素数量
        int8_t contents[];  // 元素数组,元素实际类型可能为 int16_t,int32_t,int64_t,
    } intset;

该数据结构有以下特征:

ziplist

压缩列表是为了节约内存而开发的,是存储在连续内存块上的顺序数据结构。
一个压缩列表可以包含任意多的 entry 节点,每个节点包含一个字节数组或整数。
redis 中并没有显式定义 ziplist 的数据结构,仅仅提供了一个描述结构 zlentry 用于操作数据。

    typedef struct zlentry {
        unsigned int prevrawlensize;// 用于记录前一个 entry 长度的字节数
        unsigned int prevrawlen;    // 前一个 entry 的长度
        unsigned int lensize        // 用于记录当前 entry 类型/长度的字节数
        unsigned int len;           // 实际用于存储数据的字节数
        unsigned int headersize;    // prevrawlensize + lensize
        unsigned char encoding;     // 用于指示 entry 数据的实际编码类型
        unsigned char *p;           // 指向 entry 的开头
    } zlentry;

其实际的内存布局如下:

+----------+---------+---------+--------+-----+--------+--------+
|  zlbytes |  zltail |  zllen  | entry1 | ... | entryN |  zlend |
+----------+---------+---------+--------+-----+--------+--------+
<--------------------------- zlbytes --------------------------->
                                               ^--zltail
                                <------- zllen ------->

entry 的内存布局如下:

+-------------------+----------+---------+
| prev_entry_length | encoding | content |
+-------------------+----------+---------+

该数据结构具有以下特征:

quicklist

在较早版本的 redis 中,list 有两种底层实现:

两者各有优缺点:

为了结合两者的优点,在 redis 3.2 之后,list 的底层实现变为快速列表 quicklist。
快速列表是 linkedlist 与 ziplist 的结合: quicklist 包含多个内存不连续的节点,但每个节点本身就是一个 ziplist。

    typedef struct quicklistNode {
        struct quicklistNode *prev;  // 上一个 ziplist 
        struct quicklistNode *next;  // 下一个 ziplist 
        unsigned char *zl;           // 数据指针,指向 ziplist 结构,或者 quicklistLZF 结构
        unsigned int sz;             // ziplist 占用内存长度(未压缩)
        unsigned int count : 16;     // ziplist 记录数量
        unsigned int encoding : 2;   // 编码方式,1 表示 ziplist ,2 表示 quicklistLZF
        unsigned int container : 2;  // 
        unsigned int recompress : 1;         // 临时解压,1 表示该节点临时解压用于访问
        unsigned int attempted_compress : 1; // 测试字段
        unsigned int extra : 10;             // 预留空间
    } quicklistNode;

    typedef struct quicklistLZF {
        unsigned int sz;    // 压缩数据长度
        char compressed[];  // 压缩数据
    } quicklistLZF;

    typedef struct quicklist {
        quicklistNode *head;        // 列表头部
        quicklistNode *tail;        // 列表尾部
        unsigned long count;        // 记录总数
        unsigned long len;          // ziplist 数量
        int fill : 16;              // ziplist 长度限制,每个 ziplist 节点的长度(记录数量/内存占用)不能超过这个值
        unsigned int compress : 16; // 压缩深度,表示 quicklist 两端不压缩的 ziplist 节点的个数,为 0 表示所有 ziplist 节点都不压缩
    } quicklist;

该数据结构有以下特征:

robj

为了实现动态编码技术,redis 构建了一个对象系统。
redis 可以在执行命令前,根据对象类型判断当前命令是否能够执行。
此外,该系统通过引用计数实现内存共享,并记录来对象访问时间,为优化内存回收策略提供了依据。

    typedef struct redisObject {
        unsigned type:4;        // 类型,当前对象的逻辑类型,例如:set
        unsigned encoding:4;    // 编码,底层实现的数据结构,例如:intset / ziplist
        unsigned lru:24;        /* LRU 时间 (相对与全局 lru_clock 的时间) 或
                                 * LFU 数据 (8bits 记录访问频率,16 bits 记录访问时间). */
        int refcount;           // 引用计数
        void *ptr;              // 数据指针,指向具体的数据结构
    } robj;

该数据结构有以下特征:

编码格式

string 类型

string 的编码类型可能为:

127.0.0.1:6379> SET str "1234567890 1234567890 1234567890 1234567890"
OK
127.0.0.1:6379> STRLEN str
(integer) 43
127.0.0.1:6379> OBJECT ENCODING str
"embstr"
127.0.0.1:6379> APPEND str _
(integer) 44
127.0.0.1:6379> OBJECT ENCODING str
"raw"

使用 embstr 编码是为了减少短字符串的内存分配次数,参考 redis 作者原话:

REDIS_ENCODING_EMBSTR_SIZE_LIMIT set to 39.
The new value is the limit for the robj + SDS header + string + null-term to stay inside the 64 bytes Jemalloc arena in 64 bits systems.

对比两者内存布局可以发现:


<------------------------------------------ Jemalloc arena (64 bytes)  ---------------------------------------------->
+-------------------------------------------------------------------------------+---------------------+--------------+
|                             redisObject (16 bytes)                            |  sdshdr8 (3 bytes)  |   45 bytes   |
+--------------------+---------------------------------+-------+----------+-----+-----+-------+-------+---------+----+
| type(REDIS_STRING) | encoding(REDIS_ENCODING_EMBSTR) |  lru  | refcount | ptr | len | alloc | flags |   buf   | \0 |
+--------------------+---------------------------------+-------+----------+-----+-----+-------+-------+---------+----+

+--------------------+
|    redisObject     |
+--------------------+
|        type        |
|    REDIS_STRING    |
+--------------------+
|      encoding      |
| REDIS_ENCODING_RAW |
+--------------------+      +---------+
|         ptr        | ---> | sdshdr? |
+--------------------+      +---------+
                            |   len   |
                            +---------+
                            |  alloc  |
                            +---------+
                            |  flags  |
                            +---------++---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
                            |   buf   || T | h | e | r | e |   | i | s |   | n | o |   | c | e | r | t | a |...|
                            +---------++---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+

list 类型

list 默认的编码类型为 OBJ_ENCODING_QUICKLIST quicklist

hash 类型

hash 的编码类型有 OBJ_ENCODING_ZIPLIST ziplist 与 OBJ_ENCODING_HThashtable,具体使用哪种编码受下面两个选项控制:

key 长度超过 64 的情况:

127.0.0.1:6379> HSET table x 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
(integer) 0
127.0.0.1:6379> OBJECT ENCODING table
"ziplist"
127.0.0.1:6379> HSET table x 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
(integer) 0
127.0.0.1:6379> OBJECT ENCODING table
"hashtable"
127.0.0.1:6379> DEL table
(integer) 1
127.0.0.1:6379> HSET table xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 'x'
(integer) 1
127.0.0.1:6379> OBJECT ENCODING table
"ziplist"
127.0.0.1:6379> HSET table xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 'x'
(integer) 1
127.0.0.1:6379> OBJECT ENCODING table
"hashtable"

value 长度超过 64 的情况:

127.0.0.1:6379> HSET table x 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
(integer) 0
127.0.0.1:6379> OBJECT ENCODING table
"ziplist"
127.0.0.1:6379> HSET table x 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
(integer) 0
127.0.0.1:6379> OBJECT ENCODING table
"hashtable"
127.0.0.1:6379> DEL table
(integer) 1
127.0.0.1:6379> HSET table xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 'x'
(integer) 1
127.0.0.1:6379> OBJECT ENCODING table
"ziplist"
127.0.0.1:6379> HSET table xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 'x'
(integer) 1
127.0.0.1:6379> OBJECT ENCODING table
"hashtable"

元素数量度超过 512 的情况:

127.0.0.1:6379> EVAL "for i=1,512 do redis.call('HSET', KEYS[1], i, i) end" 1 numbers
(nil)
127.0.0.1:6379> HLEN numbers
(integer) 512
127.0.0.1:6379> OBJECT ENCODING numbers
"ziplist"
127.0.0.1:6379> DEL numbers
(integer) 1
127.0.0.1:6379> EVAL "for i=1,513 do redis.call('HSET', KEYS[1], i, i) end" 1 numbers
(nil)
127.0.0.1:6379> HLEN numbers
(integer) 513
127.0.0.1:6379> OBJECT ENCODING numbers
"hashtable"

set 类型

set 的编码类型有 OBJ_ENCODING_INTSET intset 与 OBJ_ENCODING_HT hashtable,具体使用哪种编码受下面两个选项控制:

包含非整数元素的情况:

127.0.0.1:6379> SADD set 1 2
(integer) 2
127.0.0.1:6379> OBJECT ENCODING set
"intset"
127.0.0.1:6379> SADD set "ABC"
(integer) 1
127.0.0.1:6379> OBJECT ENCODING set
"hashtable"

元素数量度超过 512 的情况:

127.0.0.1:6379> EVAL "for i=1,512 do redis.call('SADD', KEYS[1], i, i) end" 1 numbers
(nil)
127.0.0.1:6379> SCARD numbers
(integer) 512
127.0.0.1:6379> OBJECT ENCODING numbers
"intset"
127.0.0.1:6379> DEL numbers
(integer) 1
127.0.0.1:6379> EVAL "for i=1,513 do redis.call('SADD', KEYS[1], i, i) end" 1 numbers
(nil)
127.0.0.1:6379> SCARD numbers
(integer) 513
127.0.0.1:6379> OBJECT ENCODING numbers
"hashtable"

zset 类型

set 的编码类型有 OBJ_ENCODING_ZIPLIST ziplist 与 OBJ_ENCODING_SKIPLISTskiplist。

使用 ziplist 编码时,每个集合元素使用两个相邻的 entry 节点保存,第一个节点保存成员值 member,第二节点保存元素的分值 score,并且 entry 按照 score 从小到大进行排序:

+----------------------+
|     redisObject      |
+----------------------+
|         type         |
|      REDIS_ZSET      |
+----------------------+
|       encoding       |
| OBJ_ENCODING_ZIPLIST |
+----------------------+      +----------+----------+---------+--------------------+-------------------+-----+-----------------------+--------------------+-------+
|          ptr         | ---> |  zlbytes |  zltail  |  zllen  | entry 1 (member 1) | entry 2 (score 1) | ... | entry 2N-1 (member N) | entry 2N (score N) | zlend |
+----------------------+      +----------+----------+---------+--------------------+-------------------+-----+-----------------------+--------------------+-------+
                                                               >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> score increase >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

使用 skiplist 实现时,使用会使用一个名为 zset 的数据结构:

    typedef struct zset {
        dict *dict;      // 维护 member -> score 的映射,查找给的成员的分值
        zskiplist *zsl;  // 按 score 大小保存了所有集合元素,支持范围操作
    } zset; // dict 与 zsl 会共享成员与分值


+----------------------+                                       +--------+     +------------+    +---------+
|     redisObject      |                                   +-->| dictht |     |  StringObj | -> |  long   |
+----------------------+                       +-------+   |   +--------+     +------------+    +---------+
|         type         |                   +-->| dict  |   |   | table  | --> |  StringObj | -> |  long   |
|      REDIS_ZSET      |                   |   +-------+   |   +--------+     +------------+    +---------+
+----------------------+                   |   | ht[0] | --+                  |  StringObj | -> |  long   |
|       encoding       |      +--------+   |   +-------+      +-----+         +------------+    +---------+
| OBJ_ENCODING_ZIPLIST |      |  zset  |   |                  | L32 | -> NULL
+----------------------+      +--------+   |                  +-----+
|          ptr         | ---> |  dict  | --+                  | ... |
+----------------------+      +--------+       +--------+     +-----+    +-----------+                     +-----------+
                              |  zsl   | --->  | header | --> | L4  | -> |     L4    | ------------------> |     L4    | -> NULL
                              +--------+       +--------+     +-----+    +-----------+                     +-----------+
                                               | tail   |     | L3  | -> |     L3    | ------------------> |     L3    | -> NULL
                                               +--------+     +-----+    +-----------+    +-----------+    +-----------+
                                               | level  |     | L2  | -> |     L2    | -> |     L2    | -> |     L2    | -> NULL
                                               +--------+     +-----+    +-----------+    +-----------+    +-----------+
                                               | length |     | L1  | -> |     L1    | -> |     L1    | -> |     L1    | -> NULL
                                               +--------+     +-----+    +-----------+    +-----------+    +-----------+
                                                                 NULL <- |     BW    | <- |     BW    | <- |     BW    |
                                                                         +-----------+    +-----------+    +-----------+
                                                                         | StringObj |    | StringObj |    | StringObj |
                                                                         +-----------+    +-----------+    +-----------+
                                                                         |    long   |    |    long   |    |    long   |
                                                                         +-----------+    +-----------+    +-----------+

zset 具体使用哪种编码受下面两个选项控制:

Redis 整体结构

每个数据库都是一个 redisDb 结构体:

    typedef struct redisDb {
        dict *dict;                 /* 据库的键空间 keyspace */
        dict *expires;              /* 设置了过期时间的 key 集合 */
        dict *blocking_keys;        /* 客户端阻塞等待的 key 集合 (BLPOP)*/
        dict *ready_keys;           /* 已就绪的阻塞 key 集合 (PUSH) */
        dict *watched_keys;         /* 在事务中监控受监控的 key 集合 */
        int id;                     /* 数据库 ID */
        long long avg_ttl;          /* 平均 TTL, just for stats */
        unsigned long expires_cursor; /* 过期检测指针 */
        list *defrag_later;         /* 内存碎片回收列表 */
    } redisDb;

redis 所有数据库都保存着 redisServer.db 数组中,redisServer.dbnum 保存了数据库的数量,简化后的内存布局大致如下:

+-------------+
| redisServer |
+-------------+    +------------+------+-------------+
|     db      | -> | redisDb[0] | .... | redisDb[15] |
+-------------+    +------------+------+-------------+
|    dbnum    |      |
|     16      |      |
+-------------+      |  +---------+                         +------------+
                     +->| redisDb |                     +-> | ListObject |
                        +---------+    +------------+   |   +------------+
                        |  dict   | -> |  StringObj | --+
                        +---------+    +------------+       +------------+
                        | expires |    |  StringObj | ----> | HashObject |
                        +---------+    +------------+       +------------+
                              |        |  StringObj | --+
                              |        +------------+   |   +------------+
                              |                         +-> | StringObj  |
                              |                             +------------+
                              |
                              |       +------------+    +-------------+
                              +---->  |  StringObj | -> |    long     |
                                      +------------+    +-------------+
                                      |  StringObj | -> |    long     |
                                      +------------+    +-------------+

至此,redis 的几种编码方式都介绍完毕,后续将对 redis 的一些其他细节进行分享,感谢观看。如果觉得本文对你有帮助,可以点赞关注支持一下!

上一篇 下一篇

猜你喜欢

热点阅读