Redis中的数据结构与对象(简单动态字符串)

本文探讨了Redis的Simple Dynamic String (SDS)技术,它提供了一种常数复杂度获取长度、防止缓冲区溢出、减少内存重分配的字符串管理方式,支持二进制安全和兼容部分C字符串函数,对比了SDS与传统C字符串的差异。
摘要由CSDN通过智能技术生成

一、简单动态字符串

1、简介
2、SDS的定义
3、SDS与C字符串的区别
4、SDS特性

1.1 简介

  Redis没有直接使用C语言传统的字符串表示(以空字符结尾的字符数组),而是自己实现一种名为简单动态字符串(simple dynamic string, SDS)的抽象类型,并将SDS用作Redis的默认字符串表示。

1.2 SDS的定义

每个sds.h/sdshdr结构表示一个SDS值:

struct sdshdr {
    // 记录buf数组中已使用字节的数量
    // 等于SDS所保存字符串的长度
    int len;
    // 记录buf数组中未使用字节的数量
    int free;
    // 字节数组,用于保存字符串
    char buf[];
};

SDS示例
SDS数据结构示例

SDS遵循C字符串以空字符串结尾的惯例,保存空字符串的1字节空间不计算在SDS的len属性里面,并且为空字符分配额外的1字节空间,以及添加空字符串末尾等操作,都是由SDS函数自动完成的,即这个空字符串对于使用者来说是完全透明的。
遵循空字符串结尾的好处是,SDS可以直接复用C字符串函数库里面的函数。

1.3 SDS与C字符串的区别

C字符串结构该图就展示了一个值为"Redis"的C字符串。

  C语言使用长度为N+1的字符数组来表示长度为N的字符串,并且字符数组的最后一个元素总是空字符’\0’。C语言使用的这种简单的字符串表示方式,并不能满足Redis对字符串在安全性、效率以及功能方面的要求。

1.4 SDS特性

1.4.1 常数复杂度获取字符串长度

由于C字符串并不记录自身的长度信息,所以为了获取一个C字符串的长度,程序必须遍历整个字符串,对遇到的每个字符进行计数,直到遇到代表字符串结尾的空字符串为,该操作的复杂度为O(N)。
和C字符串不同,因为SDS在len属性中记录了SDS本身的长度,所以获取一个SDS长度的复杂度仅为O(1)。设置和更新SDS长度的工作是由SDS的API在执行时自动完成的,使用SDS无须进行任何手动修改长度的工作。即Redis获取字符串长度的工作不会成为Redis的性能瓶颈。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uT8aMCuu-1625582340687)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9b92372e33a84c02a9e8a2e551e049a7~tplv-k3u1fbpfcp-watermark.image)]
                11字节长的SDS

1.4.2 杜绝缓冲区溢出

由于C字符串不记录自身长度带来的另一个问题是容易造成缓冲区溢出(buffer overflow)。
举个例子,<string.h>/strcat函数可以将src字符串中的内容拼接到dest字符串的末尾:
char *strcat(char *dest, const char *src);一旦没有dest分配足够的内存,就会产生缓冲区溢出。

与C字符串不同,SDS的空间分配策略完全杜绝了发生缓冲区溢出的可能性。

举个例子,SDS的API里面有一个用于执行拼接操作的sdscat函数,它可以将一个C字符串拼接到给定SDS所保存的字符串的后面,但是在执行拼接操作之前,sdscat会先检查给定SDS的空间是否足够,如果不够的话,sdscat就会先扩展sds的空间,最后再执行拼接操作。

例如,如果我们执行sdscat(s, " Cluster");

image.png sdscat函数大概执行流程如上图:
SDS示例 该图为sdscat执行之前的SDS

执行拼接" Cluster"的操作,拼接操作完成之后的SDS如下图:

image.png

注意,上图的SDS,sdscat不仅对这个SDS进行了拼接操作,还为SDS分配了13字节的未使用空间,并且拼接之后的字符串也正好是13字节长,这种现象是因为它和SDS的空间分配策略有关。下一小节将对这一策略进行说明。

1.4.3 减少修改字符串时带来的内存重分配次数

  由于C字符串的长度和底层数组的长度之间存在着关联性,每次增长或者缩短一个C字符串,程序都要对保存这个C字符串的数组进行一次内存重分配操作。比如拼接操作(append),在执行操作之前,程序必须手动通过内存重分配来扩展底层数组的空间大小,一旦忘了就会产生缓冲举出;如果执行截断操作(trim),在执行这个操作之后,必须手动释放不再使用的那部分空间,否则就会产生内存泄露。

因为内存重分配涉及复杂的算法,并且可能需要执行系统调用,所以这是一个比较耗时的操作,但对于Redis这种对速度极其严苛的数据库来说是不行的。为了避免C字符串的这种缺陷,SDS通过未使用空间解除了字符串长度和底层数组之间的关联;在SDS中,buf数组的长度不一定就是字符数量加一,数组里面可以包含未使用的字节,而这些字节的数量就由SDS的free属性记录。

通过未使用空间,SDS实现了空间预分配和惰性空间释放两种优化策略。

1、空间预分配

空间预分配主要用于优化SDS的字符串增长操作。当对一个SDS进行修改,并且需要进行空间扩展的时候,程序不仅会为SDS分配修改所有必须要的空间,还会为SDS分配额外的未使用空间。

其中,额外分配的未使用空间数量如下图:

image.png

2、惰性空间释放

惰性空间释放用于优化SDS的字符串缩短操作:当SDS的API需要缩短SDS保存的字符串时,程序并不立即使用内存重新分配来回收缩短后多出来的字节,而是使用free属性将这些字节的数量记录起来,并等待将来使用。

举个例子:sdstrim函数接受一个SDS和一个C字符串作为参数,从SDS左右两端分别移除所有在C字符串中出现过的字符。

image.png
比如对上图执行sdstrim(s, "XY");//移除SDS字符串的所有X和Y函数之后,会将SDS修改如下图:

image.png

注意执行sdstrim之后的SDS并没有释放多出来的8字节空间,而是将这8字节空间作为未使用空间保留在了SDS里面,如果后面有需要对SDS进行增长操作的话,这些未使用空间就会派上用场。
比如现要对s执行sdscat(s, " Redis")操作,那么完成这次操作将不需要执行内存重分配:因为SDS里面预留的8字节空间足够拼接6个字节长的" Redis"。

通过惰性空间释放策略,SDS避免了缩短字符串时所需的内存重新分配操作,并为将来可能有的增长操作提供了优化。与此同时,SDS为提供了相应的API,让我们可以在有需要时,真正地释放SDS的未使用空间,所以不用担心这种策略会造成内存浪费。

1.4.4 二进制安全

C字符串中的字符必须符合某种编码(比如ASCII),并且除了空字符串的末尾之外,字符串里面不能包含空字符,否则最先被程序读入的空字符串被误认为是字符串结尾,然而这些限制使得C字符串只能保存文本数据,不能保存像图片、音频、视频、压缩文件这样的二进制数据。

Redis为了确保可以适用于各种不同的场景,SDS的API都是二进制安全的(binary-safe),所有SDS API都会以处理二进制的方式来处理SDS存放在buf数组里的数据,程序不会对其中的数据做任何限制、过滤,数据在写入时是什么样的,它被读取时就是什么样。这也是我们将SDS的buf属性称为字节数组的原因————Redis不是用这个数组来保存字符,而是用来保存一系列二进制数据。

1.4.5 兼容部分C字符串函数

虽然SDS的API都是二进制安全的,但它们一样遵循C字符串以空字符结尾的惯例:这些API总会将SDS保存的数据末尾设置为空字符,并且总会在buf数组分配空间时多分配一个字节来容纳这个空字符,这都是为了让那些保存文本数据的SDS可以重用一部分<string.h>库定义的函数,从而避免了不必要的代码重复。

1.5 总结

C字符串SDS
获取字符串长度的复杂度O(N)获取字符串长度的复杂度O(1)
API是不安全的,可能会造成缓冲区溢出API是安全的,不会造成缓冲区溢出
修改字符串长度N次必然需要执行N次内存重分配修改字符串长度N次最多需要执行N次内存重分配
只能保存文本数据可以保存文本数据或者二进制数据
可以使用<string.h>库中的函数可以使用一部分<string.h>库中的函数
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值