Redis中SDS简单动态字符串问题怎么解决

一、SDS的结构

c语言没有string类型,本质是char[]数组;而且c语言数组创建时必须初始化大小,指定类型后就不能改变,并且字符数组的最后一个元素总是空字符 '

c语言没有string类型,本质是char[]数组;而且c语言数组创建时必须初始化大小,指定类型后就不能改变,并且字符数组的最后一个元素总是空字符 '
\0'

'

以下展示了一个值为 "
Redis"
的 C 字符串:

解决Redis中SDS简单动态字符串问题的几种方法

Redis没有直接使用C语言的字符串方式,而是构建了一种简单动态字符串(Simple dynamic string, SDS)的类型,Redis中的字符串底层都是使用SDS结构进行存储,比如包含字符串的键值对底层都是使用SDS结构实现的。

SDS结构定义在sds.h中

struct sdshdr{


int len;
//SDS保存的字符串长度


int free;
//buf数组中未使用字节数量


char buf[];
//字符数组,保存字符串


}

最后一个字节保存了空字符'
\0'
,保留了C字符串的规范,使得SDS结构的字符串,可以重用一部分C函数库的函数。

二、为什么不使用C字符串

主要是因为C字符串有以下缺点:

获取字符串长度时间复杂度为O(N):C字符串获取长度需遍历整个字符串,遇到'
\0'
空字符为止。如果在进行字符串追加操作时没有分配足够的内存,就会发生缓冲区溢出。 内存重分配:每次增长或者截短字符串,程序都要对保存C字符串的数组进行内存重分配操作,而内存重分配涉及复杂的算法,并可能需要执行系统调用,所以它通常比较耗时。 空字符问题:C字符串中间不能保存空格,否则程序遍历是会误认为是字符串的末尾。由于这一限制,C字符串只能用于存储文本数据,而不适用于保存二进制数据如图片、音视频和压缩文件等。

三、怎样解决C字符串问题

1、SDS通过len属性记录了SDS长度,所以获取长度的时间复杂度为O(1),即strlen命令的时间复杂度是O(1)。

2、SDS空间分配策略避免了缓冲区溢出:当对SDS进行修改时,会先检查SDS空间是否满足修改,不满足会自动扩展到所需大小,然后才执行修改。

3、较少修改字符串时内存重分配次数:SDS中的free记录buf字节数组中未使用的字节。

redis通过free属性实现空间预分配、惰性空间释放两种优化策略。

空间预分配:当对SDS进行增长操作时,程序不仅会分配修改所必须得空间,还会为SDS分配额外的未使用空间。内存重分配次数在连续执行字符串增长操作时得以减少,这是通过预分配策略实现的。 惰性空间释放:当对SDS进行截短操作时,程序并不会立即回收缩短后多出来的字节所占用的内存,而是使用free属性记录多出来的字节数,以供将来使用。未被使用的空间可能会在未来进行SDS增长时派上用场,此时增长操作不一定需要执行内存重分配。

SDS结构中的buf字节数组,是二进制安全的,不仅可以保存字符,也可以保存二进制数据。

SDS保留了C字符串的惯例,将数据的末尾设置为空字符'
\0'
,SDS中之所以保留这一规范是可以重用C字符串函数库的一部分函数,例如追加字符串。

四、对字符串的进一步优化

Redis string的三种编码:

int 存储8个字节的长整型(long,2^63-1 ) embstr, embstr格式的SDS (Simple Dynamic String) raw, raw格式的SDS,存储大于44个字节的长字符串

int类型就是指的是数字,那么raw、embstr都代表的是字符串有什么异同吗,下面我们分析下。

图中展示了两者的区别,可以看到embstr将redisObject和SDS保存在连续的64字节空间内,这样可以只需要一次内存分配,而对于raw来说,SDS和redisObject分离,需要两次内存分配,而且占用更多的内存空间。

可以看到embstr在3.2+中使用了叫sdshdr8的结构,在该结构下,元数据只需要3个字节,而Redis需要8个字节,所以总共64个字节,减去redisObject(16字节),再减去SDS的原信息,最后的实际内容就变成了44字节和39字节。

当字符串小于等于 44 字节时,Redis 就使用了嵌入式字符串的创建方法,以此减少内存分配和内存碎片。

下面这张图展示了 createEmbeddedStringObject 创建嵌入式字符串的过程:

总之,只要记住,Redis 会通过设计实现一块连续的内存空间,把 redisObject 结构体和 SDS 结构体紧凑地放置在一起。

这样一来,对于不超过 44 字节的字符串来说,就可以避免内存碎片和两次内存分配的开销了。

SDS是Redis中一种高效的字符串实现方式,它具有自动扩容、二进制安全、O(1)长度获取和修改等优点。在实际的应用中,SDS可以帮助我们实现高效的字符串操作,同时也可以避免一些常见的字符串操作问题,比如缓冲区溢出等。通过深入了解SDS的内部结构和实现原理,我们可以更好地理解Redis的底层机制,进一步提升我们的Redis应用能力。



一、背景介绍
Redis是当今流行的NoSQL数据库,其中SDS简单动态字符串是Redis中一种非常重要的数据结构。由于SDS的特殊性质,一些问题经常会困扰开发者们,本文将介绍几种解决方法。
二、问题及解决方法
1、SDS内存泄漏问题
SDS内存泄漏问题是很多开发者在使用Redis中遇到的一个老生常谈的问题。如果数据经常被修改,SDS会频繁分配和释放内存,造成内存泄漏,如果不及时处理,可能会导致Redis整个进程的崩溃。解决这个问题的方法有两种,一种是使用内存池,另一种是手动释放内存。
内存池是一种预先分配内存的机制,可以减少内存分配和释放的次数,提高内存利用率。Redis中有一个名为jemalloc的内存池工具,在搭建Redis时建议使用jemalloc,在使用内存池时,SDS中的内存分配和释放可以通过命令redis_set_allocator来注册。
手动释放内存的方法比较特殊,需要在SDS的长度小于容量的时候,调用SDS的函数进行空间的缩小,SDS会自动将多余的内存释放掉。但需要注意的是,这个方法是笨重的,可能会增加代码量。
2、SDS结构大小的问题
SDS在存储字符串时,会给字符串加上长度标志和结束符'\\0',所以SDS的结构大小会比字符串的实际长度要大。当SDS保存的字符串长度很小的时候,SDS本身的结构大小会占用大部分空间,造成内存空间的浪费。为了解决这个问题,我们需要在SDS的定义中使用一个 union 结构,当SDS保存的字符串长度不超过 21 字节时,SDS 的buf属性就不会被用到了,此时可以将buf 属性指向SDS 结构内存地址。这种方法牺牲了一定的灵活性,但可以节约内存空间,对于内存要求高的情况时,这种优化是非常划算的。
3、SDS赋值问题
在SDS的赋值中,尤其是在对SDS进行追加或者修改的时候,SDS的结构体和SDS的缓冲区大小没有同步增长,当SDS长度较大的时候,其执行性能可能会受到影响。解决这个问题可以增加容量,当SDS长度增加时,缓冲区内的空间不足的时候,可以使用空间预留技术,以减少内存分配次数。
三、总结
这篇文章介绍了三种解决SDS简单动态字符串问题的解决方法:内存池、SDS结构体大小问题、SDS赋值问题。在实际实现时,应该根据具体情况选择相对应的技术,并加以实践和验证。通过优化SDS,我们可以在保证Redis性能表现稳定的情况下,节省更多内存空间,使得Redis更加强大和具有竞争力。