当前位置: 首页 > 科技观察

Redis字符串使用简单,但原理真的不简单

时间:2023-03-14 09:42:41 科技观察

本文转载自微信公众号《Java极客技术》,作者鸭血范。转载本文请联系Java极客技术公众号。大家好,我是阿凡~不管你现在用什么编程语言,每天用得最多的应该就是字符串了。可以说字符串对象是非常基础和重要的。那么今天想和大家聊一聊Redis字符串的实现。让我们来看看这个看似简单的字符串。为什么不容易实施?看完这篇文章,你可以了解到:Redis字符串对象多重数据结构底层数据结构转换关系SDS动态字符串Redis是用C语言实现的,那么字符串为什么不直接使用C语言字符串呢?Redis对象每当我们在Redis中保存一对新的键值对时,Redis都会创建至少两个对象,一个对象用来保存键,另一个对象用来保存值。Redis中的每个对象都是一个redisObject结构。这个结构体中有3个属性:type,表示对象encoding的类型,表示编码pstr,type,指向底层数据结构,表示对象类型。目前可用的类型有:stringobjectListObjectHashObjectCollectionObjectOrderedCollectionObject我们可以使用Redis的TYPE命令查看当前key对应的value的对象类型。Redis的键对象说起来很简单,永远是字符串对象,而值对象就比较复杂了,可以是字符串,也可以是集合,也可以是字典等对象。每种对象类型的底层实现会使用多种数据结构,编码代表底层数据结构类型:我们可以通过Redis的OBJECTENCODING命令查看当前一个key-value对象的底层编码:每种对象类型可能支持A各种数据结构,关系如下:RedisstringobjectRedisstringobject底层支持三种数据结构,分别是(下面使用OBJECTENCODING输出):intembstrrawint表示一个long类型的整数,只要字符串对象存储一个整数值,将使用int。这里要注意,只有整数才用int。如果是浮点数,Redis其实是先把浮点数转换成字符串值,然后保存。embstr和raw类型的底层数据结构其实都是SDS(SimpleDynamicString),Redis内部定义了一个结构,叫做sdshdr。两者的区别在于embstr类型会调用内存分配函数分配一块连续的内存空间,其中依次包含两个数据结构redisObject和sdshdr。raw类型会调用两次内存分配函数,分配两块内存空间,一块包含redisObject结构,另一块包含sdshdr结构。SDS简单动态字符串接下来让我们看看简单的动态字符串。sdshdrsds底层结构包含三个属性:len,用于记录下方buf[]数组已用长度,等于SDSfree保存的字符串长度,用于记录buf[]未使用长度buf[]下面的数组,用户保存的是字符串这里需要注意的是,buf[]数组的最后一个字节会存放一个空字符\0,代表字符串的结束。sdshdr结构中的len长度不包含此空字符。这种设计和C语言中的字符串是一样的。SDS扩容当我们对SDS字符串进行增长操作时,如果SDS空间不足,SDS会先扩容,再进行修改操作。假设当前SDS值如下:现在执行一个字符串拼接命令,APPENDsds"Cluster"由于拼接后的字符串总长度为13,SDS剩余空间不足,所以会扩容SDS,重新分配内存再次执行。因为我们上面的例子,修改后的SDS的长度(也就是len属性值)小于1MB,那么程序会分配一个和len属性一样大小的未使用空间,而SDS中的len属性就是与自由属性值相同。此时SDSbuf数组的实际长度为:13+13+1=27如果修改后的SDS长度大于1MB,那么Redis会分配1MB的未使用空间。假设修改后,SDSlen属性变为20MB,那么程序会分配1MB未使用的空间,SDSbuf数组的实际长度为:20MB+1MB+1byte按照上面的例子,如果我们执行string再次拼接命令:APPENDsds"Guide"这次SDS未使用的空间足以保存拼接字符串,所以这次不需要重新分配内存。SDSlazyspacerelease当我们减少字符串的时候,SDS的字符串值会变短,所以SDS的剩余空间会增加。这时候程序不会立即使用内存分配函数回收多余的空间,而是使用free属性记录多余的空间,以备后用。通过这种惰性空间释放策略,SDS避免了字符串缩短所需的内存重新分配操作,后续SDS字符串增长可以直接使用多余的空间。当然,SDS也有相应的API,可以真正释放SDS未使用的空间,不用担心惰性释放策略带来的内存浪费。为什么Redis不直接使用C语言的字符串呢?Redis底层使用C语言编程,所以其实C语言也有字符串,Redis完全可以复用C语言字符串~那为什么Redis要闭门造车重新设计一个SDS数据结构呢?这是因为C语言的简单字符串形式在安全性、效率和功能性方面不符合Redis的要求。首先,如果我们需要获取C语言字符串的长度,可以使用如下函数:strlen(str)strlen函数实际上是采用遍历的方式对每个字符进行计数,直到遇到代表字符串结束的空字符字符串。这个操作的时间复杂度是O(N)。在SDS中,由于len记录的是当前字符串的长度,所以可以直接读取,时间复杂度仅为O(1)。这样就保证了获取字符串长度的操作不会成为Redis的性能瓶颈。第二点,C语言中的字符串每次增加或缩短时,都会重新分配内存,否则可能导致缓冲区溢出或内存泄漏。但是由于SDS采用了空间预分配的策略,如果SDS连续增长N次,那么内存重新分配的次数就会从N次减少到最多N次。第三,C语言字符串中不能包含空字符,否则第一个空字符会被误认为是字符串的结尾,大大限制了使用场景。在SDS中,由于可以通过len属性的值来判断字符串是否结束,就没有这样的麻烦了。所以SDS字符串是二进制安全的。字符串对象底层数据结构的SDS结构转换需要额外的属性记录长度和未使用长度。这虽然降低了系统的复杂度,提高了性能,但还是要付出相应的代价,即存在一定的内存空间浪费。因此,并不是所有Redis字符串对象的底层结构都使用SDS。如果字符串对象持有整数值,则整数值将直接存储在字符串对象结构的ptr属性中。如果保存的不是整数,但字符串的长度小于等于39字节,那么该字符串将使用embstr编码。最后只有不满足以上两种情况才会使用raw编码方式。另外int编码和embstr在一定条件下也会转为raw编码格式。如果对底层整数执行附加操作,使其不再是整数,则字符串对象的编码从int变为raw。对于embstr编码,其实是只读的,因为Redis底层没有提供任何修改embstr编码对象的程序。所以一旦我们修改了embstr编码的字符串,字符串对象的编码就会从embstr变成raw。综上所述,Redis字符串是操作频率最高的数据结构。理解底层的字符串对象对于我们理解Redis是非常重要的。Redis字符串对象的底层结构可以分为整数和SDS。我们在操作set命令保存整数的时候,会直接使用整数。SDS是Redis内部定义的另一种结构。与C语言的字符串相比,它可以快速计算出字符串的长度,不需要频繁的内存分配。最后,SDS是一个二进制安全字符串。虽然SDS的设计带来了很多优势,但是与C语言相比,这种结构至少占用4(len)+4(free)=8字节的内存。如果buf[]数组中还有未使用的空间,空间浪费就更厉害了。所以Redis的字符串并不是万能的,在某些场景下可能会占用大量的内存。但是如果你改变数据结构,内存使用可能会更少。因此,我们必须根据合适的场景使用合适的数据组织。