引言
本文是对《redis设计与实现(第二版)》中数据结构与对象相关内容的整理与说明。本篇文章只对对象结构,1种对象——字符串对象。以及字符串对象所对应的两种编码——raw和embstr,进行了详细介绍。表达一些本人的想法与看法,也希望更多朋友一起来讨论,分享交流。
作者:太阳
云掣科技-数据库团队
数据库工程师
对象
redis使用对象来表示数据库中的键和值,每次当我们在redis的数据库中新创建一个键值对时,我们至少会创建两个对象,一个对象用作键值对的键(键对象),另一个对象用作键值对的值(值对象)。
redis的每种对象都由对象结构(redisObject)与对应编码的数据结构组合而成,redis支持5种对象类型,分别是字符串(string)、列表(list)、哈希(hash)、集合(set)、有序集合(zset),而每种对象类型至少对应两种编码方式,不同的编码方式所对应的底层数据结构是不同的。
每个对象会用到的编码以及对应的数据结构详见下表:
每种对象对应两至三种编码,除skiplist编码需要用到两种数据结构(字典+跳跃表)外,其余编码均用到一种底层的数据结构。
同一个对象类型,在不同的场景下用到的编码(数据结构)不同,redis支持8种编码以及8种底层的数据结构。这种方式更加灵活,可以帮助redis获得更高的性能以及尽量占用更少的内存。比如如果字符串对象中要存储的字符串内容所占字节较小,会用embstr编码的格式,如果要存储的内容所占字节较大,会用raw编码的格式,具体细节后文会详细说明。
总结说明
上文说过,redis中的键和值都是由对象组成的,而对象是由对象结构和数据结构共同组成的。redis中的键,都是用字符串来存储的,即对于redis数据库中的键值对来说,键总是一个字符串对象,而值可以是字符串对象、列表对象、哈希对象、集合对象或者有序集合对象中的其中一种。
键、值的整体大致结构可以如下图所示:
对象结构
对象结构(redisObject)共有5个属性,分别是type属性、encoding属性、ptr属性、refcount属性、lru属性。
其中type属性、encoding属性、ptr属性和保存数据有关:
type属性:表示该对象的类型是什么;
encoding属性:表示这个对象使用的底层数据结构是什么;
ptr属性:是一个指向底层数据结构的指针;
refcount属性是一个引用计数属性,可以用于内存回收和对象共享;
lru属性,记录了对象最后一次被命令程序访问的时间,可以计算出某个键的空转时长。
对象结构的逻辑图如下所示:
内存回收--refcount属性
在对象结构中,有refcount这个属性,该属性用于记录对象的引用计数信息,redis利用引用计数(referencecounting)技术实现内存回收机制,通过这一机制,程序可以通过跟踪对象的引用计数信息,在适当的时候自动释放对象并进行内存回收。
具体策略:
在创建一个新对象时,引用计数的值会被初始化为1;
当对象被一个新程序使用时,它的引用计数值会被+1;
当对象不再被一个程序使用时,它的引用计数值会被-1;
当对象的引用计数值变为0时,对象所占用的内存会被释放。
对象共享--refcount属性
Redis会在初始化服务器时,服务器会创建一万个字符串对象,这些对象包含了从0到9999的所有整数值,当服务器、新创建的键需要用到值为0到9999的字符串对象时,服务器就会使用这些共享对象,而不是新创建对象。
对象结构中,refcount是引用指针属性,如果有N个键共享一个值,refcount对应的值就为N。创建共享字符串对象的数量可以通过redis.h/redis_shared_intengers常量来修改。object refcount命令可以查看某个键对应的值被引用了多少次。
让多个键共享一个值,需要执行以下两个步骤:
将键的值指针,指向被共享的值对象;
被共享的值对象的引用计数器加一,即refcount属性的值加一。
引用数为2的共享对象结构图如下图所示:
总结说明
当服务器考虑将一个键的值引用共享对象时,键的值作为目标对象,程序需要先检查共享对象和目标对象的类型是否完全相同,只有在完全相同的情况下,共享对象才会被引用。而一个共享对象保存的值越复杂,验证共享对象与目标对象所需的复杂度就会越高,消耗的CPU时间也会越多。
所以共享对象的优点是被其它键引用时,可以节省内存空间,缺点是被引用时需要进行判断,这个过程需要消耗CPU,如果共享对象简单,消耗很小的CPU并节省内存空间是值得的。
但如果对象共享很复杂,进行判断就需要消耗大量CPU,消耗大量CPU去节省内存空间是不值得的,因为redis本身的内存空间还是很大的。
知识点
redis支持5种对象,包括字符串对象、列表对象、哈希对象、集合对象以及有序集合对象。而字符串对象是redis中的一个基础对象,其它对象均可以在底层的数据结构内部嵌套字符串对象。
对于对象共享:
1、只有字符串对象才能被创建为共享对象,被其它字符串键使用;
2、用字符串对象创建的共享对象,不单单只有字符串键可以使用,那些在数据结构中嵌套了字符串对象的对象(linkedlist编码的列表对象、hashtable编码的哈希对象、hashtable编码的集合对象,以及skiplist编码的有序集合对象)都可以使用这些字符串共享对象。
Q&A
Q
为什么redis不共享列表对象、哈希对象、集合对象、有序集合对象,只共享字符串对象?
A
列表对象、哈希对象、集合对象、有序集合对象,本身可以包含字符串对象,复杂度较高。
如果共享对象是保存字符串对象,那么验证操作的复杂度为O(1);
如果共享对象是保存字符串值的字符串对象,那么验证操作的复杂度为O(N);
如果共享对象是包含多个值的对象,其中值本身又是字符串对象,即其它对象中嵌套了字符串对象,比如列表对象、哈希对象,那么验证操作的复杂度将会是O(N的平方);
如果对复杂度较高的对象创建共享对象,需要消耗很大的CPU,用这种消耗去换取内存空间,是不合适的。
碎碎念
1、现在我们知道,redis为了避免额外的内存消耗,在初始化的时候,为0~9999这些整数创建了共享对象。那除了0~9999,redis内部是否还设置了其它类型的共享对象?但具体有哪些值被作为了共享对象还不是特别清楚,不过应该都是一些简单的值。
2、另外,0~9999整数是程序初始化时自动创建为共享对象的,我们是否可以手动创建共享对象?比如我们认为有很多键对应的值都是相同的,是否可以手动创建共享对象以节省内存?如果可以,又有哪些限制要求?
创建的共享对象,当其它键去引用共享对象时,需要进行判断,两者的类型完全相同才可以被应用,共享对象保存的内容越复杂,进行判断时需要消耗的CPU就越大。
redis初始化创建的0~9999的共享对象,结构很简单,进行判断时消耗的CPU很小。但是如果redis允许我们手动为某些值创建共享对象,它的结构只要稍微复杂一些,就需要消耗很大的CPU,这无疑是不合适的,所以redis为了避免这种不必要的影响,应该不支持手动创建共享对象。
欢迎各位共同参与讨论
一起交流沟通~