redis专题十七:再聊一聊redis的简单动态字符串
前面开篇,我们聊到了redis的常见数据结构,也熟悉set msg "hello world"这样的命令。本篇,再聊一聊基本数据类型中的String.
Redis没有直接使用C语言的字符串(以空字符串结尾的字符数组),而是自己构建了简单的动态字符串(SDS:simple dynamic string)的抽象类型。
1. SDS的定义
sds.h/sdshdr结构如下:
typedef char *sds; struct sdshdr { // 记录 buf 数组中已使用字节的数量 // 等于 SDS 所保存字符串的长度 int len; // 记录 buf 数组中未使用字节的数量 int free; // 字节数组,用于保存字符串 char buf[]; };
- len=0时,说明SDS没有分配任何未使用的空间
- len=5,代表SDS保存了一个5字节长的字符串
- buf时一个字符数组,如'R','E','D','I','S', 最后一个字节保存空字符'\0'
SDS同样以空字符结尾,但是空字符的1字节空间不算在len属性的长度。并且这个分配额外1字节空间的操作由SDS函数自动完成。
以空字符结尾的好处可以方便直接使用C字符串函数库里面的函数。
2. SDS和C字符串的区别
首先应该先了解到,C语言使用长度为N+1的字符数组来表示长度为N的字符串,字符数组最后一位是空字符'\0'。
2.1 获取字符串长度的复杂度
C字符串 并不记录自己的长度信息,要获取其长度,只能遍历所有字符,直到遇到空字符为止,复杂度O(n)。
SDS在len属性中记录了SDS本身的长度,所以复杂度为O(1)。设置和更新SDS长度的工作由SDS的API完成,使用SDS不需要任何手动修改长度的工作。
这样一来,确保获取字符串长度的工作不会成为redis的瓶颈。
2.2 杜绝缓冲区溢出
C字符串不记录自身长度带来的一个问题就是容易造成缓冲区溢出(buffer overflow)。
举例:C的strcat函数: char *strcat(char *dest, char *src)
在做字符拼接的时候,已经为dest分配了足够的内存,可以容纳src的所有内容,一旦这个不成立,就会发生缓冲区溢出,导致保存的内容被意外的修改。
SDS的空间分配策略就解决了这个问题。当SDS的API需要对SDS进行修改时,会先检查SDS的空间是否满足修改所需要的要求,不满足,会自动扩容至执行修改所需要的大小,然后才执行修改操作。所以使用SDS不需要手动修改SDS所需空间的大小,也不会出现上述缓冲区溢出的问题。
2.3 减少修改字符串时,内存带来的内存重分配次数
以C字符串而言:
- 如果执行字符串拼接,程序需要在执行之前先通过内存重分配来扩展底层数组的空间大小,否则可能会出现缓冲区溢出
- 如果是缩短字符串,程序需要在执行之前通过内存重分配来释放字符串不再使用的那部分空间,否则可能会产生内存泄漏。
如果字符串频繁被修改,每次都需要执行一次内存重分配,光是这个步骤就会消耗一大部分时间。为了解决这个问题,SDS通过未分配空间解除了字符串长度和底层数组长度的关联。在SDS中,buf数组的长度不一定是字符数量加1,因为包括了未被使用的字节,就是我们上述提到的free属性。
对未分配空间,SDS有空间预分配和惰性空间释放两种策略。
2.3.1 空间预分配
空间预分配用来优化字符串增长。当发生修改,不仅会为SDS分配修改所需要的空间,还会为SDS分配额外未使用的空间。
- 如果修改后,SDS的len小于1M,那么程序分配free的空间和len相同。即如果修改之后len为15字节,则buf实际长度15+15+1 = 31字节
- 如果修改后,SDS的len大于1M,程序分配free空间1M。即如果修改后len为10M,则buf实际长度10M + 1M +1 byte
2.3.2 惰性空间释放
惰性空间释放主要用于优化字符串缩短。当执行修改时,不立即通过内存分配策略来回收多出来的字节,而是使用free属性将其记录下来,等待将来使用。当然,SDS也提供了相关的API,可以让我们真正释放掉未使用的空间。
2.4 二进制安全
C字符串必须符合一定的编码规范,如ASCII,且除了字符串末尾,不能包含空字符,否则会影响长度的判断,所以C也之只能保存文本相关的数组。
而SDS都会以处理二进制的方式来处理buf中的数据,程序不会对其进行任何限制过滤,所以redis还可以保存非文本以外的二进制数据。
2.5 兼容部分C函数
SDS的buf末尾设置空字符,可以方便使用C的一些函数,而不用自己写一套类似的了。
以上,做一个对比总结:
C字符串 | SDS |
获取字符串长度复杂度O(n) | 获取字符串长度复杂度O(1) |
API可能会造成缓冲区溢出 | API不会造成缓冲区溢出 |
修改字符串长度N次必然有N次内存重分配 | 修改字符串长度N次最多有N次内存重分配 |
只能保存文本数据 | 可以保存除文本外的其他二进制数据 |
可以使用所有<string.h>库中的函数 | 可以使用部分<string.h>库中的函数 |
以上就是本篇内容,其他SDS函数可参考博客:https://blog.csdn.net/u010765526/article/details/89071207
3. 总结
Redis只会用C字符串作为字面量,即一些不会对字符串进行修改的地方,如打印日志。在大多数情况下,都使用SDS作为字符串表示。
除了用来保存数据库中的字符串值外,SDS还被用做缓冲区,如AOF中的缓冲区,以及客户端状态中的输入缓冲区。