在以前的文章中,我们介绍了 Redis 用到的主要数据结构,比如简单动态字符串、双端链表、字典、压缩列表、整数集合。
然而 Redis 并没有直接使用这些数据结构来实现键值对的数据库,而是在这些数据结构之上又包装了一层 RedisObject(对象),RedisObject 有五种对象:字符串对象、列表对象、哈希对象、集合对象和有序集合对象。
还是跟以前一样,看几个问题:
使用 RedisObject 对象而不是直接使用双端队列、双端链表等数据结构,有什么好处呢?
RedisObject 的具体结构是什么?
五种对象(string、hash、list、set、sort set)对应的 RedisObject 对象有何不同,底层使用的数据结构是什么?
使用 RedisObject 的好处使用 RedisObject 的优点主要有两个,分别是:
通过不同类型的对象,Redis 可以在执行命令之前,根据对象的类型来判断一个对象是否可以执行给定的命令。
我们可以针对不同的使用场景,为对象设置不同的实现,从而优化内存或查询速度。
RedisObject 的具体结构是什么?RedisObject 的源码如下:
typedef struct redisObject { // 类型 unsigned type:4; // 编码 unsigned encoding:4; // 对象最后一次被访问的时间 unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */ // 引用计数 int refcount; // 指向实际值的指针 void *ptr; } robj;下面分别解释一下各个字段的含义:
type
type 记录了对象的类型,所有的类型如下(出自《Redis设计与实现第二版》第八章:对象):
对于 Redis 数据库保存的键值对来说,键一定是一个字符串对象,而值则可以使五种对象的其中一种。
ptr 指针:指向对象的底层实现数据结构;
encoding
encoding 表示 ptr 指向的具体数据结构,即这个对象使用了什么数据结构作为底层实现。
encoding 的取值范围如下(出自《Redis设计与实现第二版》第八章:对象):
每种类型的对象都至少使用了两种不同的编码,对象和编码的对应关系如下(出自《Redis设计与实现第二版》第八章:对象):
refcount
refcount 表示引用计数,由于 C 语言并不具备内存回收功能,所以 Redis 在自己的对象系统中添加了这个属性,当一个对象的引用计数为0时,则表示该对象已经不被任何对象引用,则可以进行垃圾回收了。
扩展一下:Java中由于引用计数法解决不了循环引用的问题,所以 Java 中使用了可达性分析算法。那么 Redis 有没有考虑循环引用的问题呢?
lru:表示对象最后一次被命令程序访问的时间。
五种对象对应的 RedisObject 字符串对象(string)字符串对象的 encoding 有三种,分别是:int、raw、embstr。
如果一个字符串对象保存的是整数值,并且这个整数值可以用 long 类型标识,那么字符串对象会讲整数值保存在 ptr 属性中,并将 encoding 设置为 int。
假设有如下命令:set number 10086。那么 number 键对象的示意图如下(出自《Redis设计与实现第二版》第八章:对象):
如果字符串对象保存的是一个字符串值,并且这个字符串的长度大于 32 字节,那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串值,并将对象的编码设置为 raw。
使用 raw 存储字符串的示意图如下(出自《Redis设计与实现第二版》第八章:对象):
如果字符串对象保存的是一个字符串值,并且这个字符串的长度小于等于 32 字节,那么字符串对象将使用 embstr 编码的方式来保存这个字符串。
使用 embstr 存储字符串的示意图如下(出自《Redis设计与实现第二版》第八章:对象):
既然有了 raw 的编码方式,为什么还会有 embstr 的编码方式呢?
因为 embstr 的编码方式有一些优点,如下:
embstr 编码将创建字符串对象所需的内存分配次数从 raw 编码的两次降低为一次。
释放 embstr 编码的字符串对象只需要调用一次内存释放函数,而释放 raw 编码的字符串对象需要调用两次内存释放函数。