Redis 的 redisObject 对象结构

摘要:redis 的 redisObject 对象结构,redis 会把我们的 value 都用一个 redisObject 对象存储,然后每个数据类型都有自己的内部编码,根据存储对象的长度来选择不同的编码进行优化,redis 还会建立一个共享对象池,用于默认 0 - 9999 范围数字对象之间的共享。

redisObject 对象结构

redis 使用对象来表示数据库中的键和值,每次在 redis 的数据库中创建一个新的键值对时,我们都会至少会创建两个对象,一个对象用作键值对的键(键对象), 另一个对象用作键值对的值(值对象)。比如使用 set 创建了一个 key => value 的键值对,那么键是一个包含了字符串 “key” 的对象,而值是一个包含了字符串值 “value” 的对象。

127.0.0.1:6379> set key value
OK

redis 的每个 value 对象存储结构如下,我们称这个结构为 redisObject 对象结构,在 redis 的源码 server.h 文件大概 603 行处,可以看到 redisObject 这个结构体,并且取了一个别名 robj

typedef struct redisObject {
    unsigned type:4; /* 对象类型 */
    unsigned encoding:4; /* 内部编码类型 */
    unsigned lru:LRU_BITS; /* 最后一次被命令访问的时间
                            * LRU time (relative to global lru_clock) or
                            * LFU data (least significant 8 bits frequency
                            * and most significant 16 bits access time). */
    int refcount; /* 当前对象的引用计数器 */
    void *ptr; /* 数据指针,指向真正的数据 */
} robj;


对象结构:type

type 字段表示当前对象使用的数据类型,比如 string、hash、list、set、zset 等。可以使用 type key 命令来查看每个 key 存储的数据是什么类型。

# string
127.0.0.1:6379> set k1 redis
OK
127.0.0.1:6379> type k1
string

# list
127.0.0.1:6379> rpush k2 1 2 3 4 5
(integer) 5
127.0.0.1:6379> type k2
list

# hash
127.0.0.1:6379> hmset k3 hash_k1 hash_v1 hash_k2 hash_v2
OK
127.0.0.1:6379> type k3
hash

# set
127.0.0.1:6379> sadd k4 a b c d e f
(integer) 6
127.0.0.1:6379> type k4
set

# zset
127.0.0.1:6379> zadd k5 4 linux 2 nginx 3 mysql 1 php
(integer) 4
127.0.0.1:6379> type k5
zset


对象结构:encoding

encoding 字段表示当前对象在 redis 内部的编码类型,在 redis 源码 server.h 文件大概 586 行处,可以看到 encoding 的常量值如下:

#define OBJ_ENCODING_RAW 0     /* Raw representation */
#define OBJ_ENCODING_INT 1     /* Encoded as integer */
#define OBJ_ENCODING_HT 2      /* Encoded as hash table */
#define OBJ_ENCODING_ZIPMAP 3  /* Encoded as zipmap */
#define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */
#define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
#define OBJ_ENCODING_INTSET 6  /* Encoded as intset */
#define OBJ_ENCODING_SKIPLIST 7  /* Encoded as skiplist */
#define OBJ_ENCODING_EMBSTR 8  /* Embedded sds string encoding */
#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */
#define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */

redis 常用的五种数据类型中,每种数据类型内部都有自己不同的存储编码,并至少可以采用两种编码方式来实现,而 encoding 代表当前对象采用哪种数据结构实现,编码的不同将直接影响数据的内存占用和读写效率。

# string 
OBJ_ENCODING_INT
OBJ_ENCODING_EMBSTR 
OBJ_ENCODING_RAW

# list
OBJ_ENCODING_LINKEDLIST
OBJ_ENCODING_ZIPLIST
OBJ_ENCODING_QUICKLIST

#hash
OBJ_ENCODING_ZIPLIST
OBJ_ENCODING_HT

#set 
OBJ_ENCODING_INTSET
OBJ_ENCODING_HT

#zset
OBJ_ENCODING_ZIPLIST
OBJ_ENCODING_SKIPLIST

我们可以使用 object encoding key 来查看当前对象用什么编码存储。

127.0.0.1:6379> object encoding key
"raw"

redis 使用不同的编码对数据的存储有两大好处:

1 可以改进内部的存储编码,但是对外部的数据结构没有影响。

2 不同的内部编码实现,可以在不同的场景发挥各自的优势。


对象结构:lru

lru 字段记录对象最后一次被访问的时间,当配置了 maxmemory 和 maxmemory-policy=volatile-lru 或者 allkeys-lru 时,lru 字段可以用于辅助 lru 算法的内存淘汰机制删除键数据。可以使用 object idletime key 命令在不更新 lru 字段情况下查看当前键的最后访问时间。

127.0.0.1:6379> object idletime key
(integer) 75740

在内存优化方面,可以使用 scan + object idletime + lua 批量查询那些 key 长时间未访问,并定时最这些 key 进行清理,可以降低内存占用。


对象结构:refcount

refcount 字段记录当前对象被引用的次数,当 refcount=0 时,reids 会回收当前对象。可以使用 object refcount key 获取当前对象引用的次数。

127.0.0.1:6379> set k1 redis
OK
127.0.0.1:6379> object refcount k1
(integer) 1


对象结构:*ptr 字段

ptr 字段与对象的数据内容相关,如果是整数,直接存储数据,否则表示指向数据的指针。redis 在 3.0 之后对值对象是字符串且长度 <=39 字节的数据,内部编码为 embstr 类型,字符串 sds 和 redisObject 一起分配,从而只要一次内存操作即可。

所以高并发写入场景中,在条件允许的情况下,建议字符串长度控制在 39 字节以内,减少创建 redisObject 内存分配次数,从而提高性能。


对象共享

redis 还具备对象共享功能,就是依靠 redisObject 对象结构中的 refcount 字段来实现的。redis 在内部维护着一个 0 - 9999 每个整数对象用于共享内存,可以通过 redis 源码 server.h 文件中 OBJ_SHARED_INTEGERS 常量来控制。

#define OBJ_SHARED_INTEGERS 10000

对象共享就是 redis 服务器已经把这些 0 - 9999 个数字对象创建好了,如果客户端需要使用范围内的数字对象可以直接使用,redis 并不需要给这些重复使用的数字对象分配内存,所以说在 redis 内存优化方面,我们可以尽量存储 0 - 9999 范围的数字来节约内存。

127.0.0.1:6379> set k1 100
OK
127.0.0.1:6379> object refcount k1
(integer) 2147483647
127.0.0.1:6379> set k2 100
OK
127.0.0.1:6379> object refcount k2
(integer) 2147483647
127.0.0.1:6379> set k3 10000
OK
127.0.0.1:6379> object refcount k3
(integer) 1
#define OBJ_SHARED_REFCOUNT INT_MAX

可以看到 k1 和 k2 是在 0 - 9999 之间的数字,所以引用计数值为 INT_MAX(2147483647) 值,但是如果对象超出 9999 范围等于 10000 的时候,比如 k3 的 refcount 返回的是 (integer) 1,就不会使用对象内存。

另外, 这些共享对象不单单只有字符串键可以使用, 那些在数据结构中嵌套了字符串对象的对象(linkedlist 编码的列表对象、 hashtable 编码的哈希对象、 hashtable 编码的集合对象、以及 zset 编码的有序集合对象)都可以使用这些共享对象。


为什么 Redis 不共享包含字符串的对象?

因为数字的复用概率最大,其次就是对于使用对象共享的关键问题就是判断相等性,redis 之所以只能使用数字共享,因为数字的比较算法复杂度是 o (1),只保留 10000 个对象共享也是防止对象池浪费。如果判断的是字符串,那么比较的复杂度是 o (n),特别是长字符串更消耗性能。对于更复杂的数据结构,比如 hash、list 等,相等比较复杂度需要 o (n ^ 2)。对于单线程的 reids 来说,这显然不合理。


参考资料:

对象的类型与编码

渐进式解析 Redis 源码 - 对象 object

结束语:感谢您对本网站文章的浏览,欢迎您的分享和转载,但转载请说明文章出处。
Top