我们在前文已经阐述了Redis 5种基础数据类型详解,分别是字符串(string)、列表(list)、哈希(hash)、集合(set)、有序集合(zset),以及5.0版本中Redis Stream结构详解;那么这些基础类型的底层是如何实现的呢?Redis的每种对象其实都由对象结构(redisObject) 与 对应编码的数据结构组合而成, 本文主要介绍对象结构(redisObject) 部分。
我在整理Redis底层设计时,发现网上其实是有很多资料的,但是缺少一种自上而下的承接。这里我将收集很多网上的资料并重新组织,来帮助你更好地理解Redis底层设计。@pdai
那么这些基础类型的底层是如何实现的呢?带着这个问题我们来着手理解底层设计,首先看下图:
它反映了Redis的每种对象其实都由对象结构(redisObject) 与 对应编码的数据结构组合而成,而每种对象类型对应若干编码方式,不同的编码方式所对应的底层数据结构是不同的。
所以,我们需要从几个个角度来着手底层研究:
为什么Redis会设计redisObject对象?
在redis的命令中,用于对键进行处理的命令占了很大一部分,而对于键所保存的值的类型(键的类型),键能执行的命令又各不相同。如: LPUSH 和 LLEN 只能用于列表键, 而 SADD 和 SRANDMEMBER 只能用于集合键, 等等; 另外一些命令, 比如 DEL、 TTL 和 TYPE, 可以用于任何类型的键;但是要正确实现这些命令, 必须为不同类型的键设置不同的处理方式: 比如说, 删除一个列表键和删除一个字符串键的操作过程就不太一样。
以上的描述说明, Redis 必须让每个键都带有类型信息, 使得程序可以检查键的类型, 并为它选择合适的处理方式.
比如说, 集合类型就可以由字典和整数集合两种不同的数据结构实现, 但是, 当用户执行 ZADD 命令时, 他/她应该不必关心集合使用的是什么编码, 只要 Redis 能按照 ZADD 命令的指示, 将新元素添加到集合就可以了。
这说明, 操作数据类型的命令除了要对键的类型进行检查之外, 还需要根据数据类型的不同编码进行多态处理.
为了解决以上问题, Redis 构建了自己的类型系统, 这个系统的主要功能包括:
redisObject 是 Redis 类型系统的核心, 数据库中的每个键、值, 以及 Redis 本身处理的参数, 都表示为这种数据类型.
- /*
- * Redis 对象
- */
- typedef struct redisObject {
-
- // 类型
- unsigned type:4;
-
- // 编码方式
- unsigned encoding:4;
-
- // LRU - 24位, 记录最末一次访问时间(相对于lru_clock); 或者 LFU(最少使用的数据:8位频率,16位访问时间)
- unsigned lru:LRU_BITS; // LRU_BITS: 24
-
- // 引用计数
- int refcount;
-
- // 指向底层数据结构实例
- void *ptr;
-
- } robj;
下图对应上面的结构
其中type、encoding和ptr是最重要的三个属性。
- /*
- * 对象类型
- */
- #define OBJ_STRING 0 // 字符串
- #define OBJ_LIST 1 // 列表
- #define OBJ_SET 2 // 集合
- #define OBJ_ZSET 3 // 有序集
- #define OBJ_HASH 4 // 哈希表
-
- /*
- * 对象编码
- */
- #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 /* 注意:版本2.6后不再使用. */
- #define OBJ_ENCODING_LINKEDLIST 4 /* 注意:不再使用了,旧版本2.x中String的底层之一. */
- #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 */
下图展示了redisObject 、Redis 所有数据类型、Redis 所有编码方式以及底层数据结构之间的关系(pdai:从6.0版本中梳理而来):
注意:OBJ_ENCODING_ZIPMAP没有出现在图中,因为在redis2.6开始,它不再是任何数据类型的底层结构(虽然还有zipmap.c的代码); OBJ_ENCODING_LINKEDLIST也不支持了,相关代码也删除了。
空转时长:当前时间减去键的值对象的lru时间,就是该键的空转时长。Object idletime命令可以打印出给定键的空转时长
如果服务器打开了maxmemory选项,并且服务器用于回收内存的算法为volatile-lru或者allkeys-lru,那么当服务器占用的内存数超过了maxmemory选项所设置的上限值时,空转时长较高的那部分键会优先被服务器释放,从而回收内存。
那么Redis是如何处理一条命令的呢?
当执行一个处理数据类型命令的时候,redis执行以下步骤
比如现在执行LPOP命令:
redis一般会把一些常见的值放到一个共享对象中,这样可使程序避免了重复分配的麻烦,也节约了一些CPU时间。
redis预分配的值对象如下:
注意:共享对象只能被字典和双向链表这类能带有指针的数据结构使用。像整数集合和压缩列表这些只能保存字符串、整数等自勉之的内存数据结构
为什么redis不共享列表对象、哈希对象、集合对象、有序集合对象,只共享字符串对象?
如果对复杂度较高的对象创建共享对象,需要消耗很大的CPU,用这种消耗去换取内存空间,是不合适的
redisObject中有refcount属性,是对象的引用计数,显然计数0那么就是可以回收。