《Redis设计与实现》Part1:简单动态字符串
Redis 没有直接使用 C 语言的字符串表示,而是定义了一种称为 SDS(simple dynamic string)的类型,并作为 Redis 默认字符串的表示。那么 Redis 为什么要这么做呢?有什么好处呢?接下来咱们就探讨一下。
SDS 定义
src/sds.h/sdshdr
1 | struct sdshdr { |
图 2.1 展示了一个字符串“Redis”的例子:
len
为 5,是字符串“Redis”的长度,表示SDS
保存了一个长度为 5 字节的字符串。free
为 0,表示SDS
无分配未使用的空间。buf
是一个char
类型的数组,数组前五个字节依次为R
、e
、d
、i
、s
,最后一个字节为空字符\0
。
可以看到SDS
遵循了C字符以空字符结尾的惯例,这么做的好处是可以重用一部分C字符串中的函数(比如printf
)。
SDS 优点
传统C字符串使用长度为 N+1 的字符数组来表示 N 个字符,并以空字符\0
结尾,这种简单的字符串表达方式并不能满足Redis对字符串的要求。
获取字符串长度
- C 字符串需要遍历整个字符串,直到遇到空字符串
\0
,所以复杂度为O(n)
- SDS 直接访问属性
len
,复杂度为O(1)
缓冲区溢出
char *strcat(char *dest, const char *src)
- C 字符串执行拼接操作时,如未分配足够空间,则可能越界溢出到其它已经被使用的空间
- SDS 执行
sdscat
函数用于字符串拼接,执行sdscat
操作之前会自动检查 SDS 是否有足够空间,不够则会先对 SDS 空间进行扩展
减少内存分配次数
内存调用是一个比较耗时的操作,涉及到复杂的算法以及可能执行系统调用,所以一半程序中,如果字符修改情况比较少,那么每次修改字符串则执行一次内存分配是可以接受的,但是在 redis 这种高性能数据库场景,必须要速度快同时数据会被频繁修改,如此一来,频繁执行内存调用进行内存的重分配则会对性能造成极大影响。
strcat 场景
假定字符串s值为“Redis”,现需要将s修改为“Redis Cluster”,如果是 C 字符串,则需要先分配增加至少 8 个字节的空间,再将字符串的实际值写入对应地址。那么 SDS 是如何应对这一场景的呢?
SDS 通过空间预分配 ,也即当 SDS 通过 api 操作需要对 SDS 空间进行扩展的时候,SDS 不仅会分配所必须的空间,还会额外预先分配未使用的空间,从而达到减少内存分配次数的目的。
分配公式:
- 修改后如果 SDS 的长度(len 属性)小于 1M,那么则分配与 len 一样大小的未使用空间
- 修改后如果 SDS 的长度大于大于等于 1M,那么固定分配 1M 的未使用空间
缩短字符串
如果要将字符串“Redis Cluster”缩短为“Redis”,SDS 会通过惰性空间释放 的策略,保留字符串缩短后多余的空间,不会立即将其释放,只是更改 free 属性的值,记录当前 SDS 剩余可用空间的字节数。通过惰性空间释放 策略,SDS 避免了字符串缩短时的重新空间分配操作,同时优化了后续字符串的增产操作。
二进制安全
C 字符串必须符合某种编码,同时字符串中间不能有空字符串”\0”,这些限制使得 C 字符串只能保存文本数据,不能保存图像、音频、视频等二进制数据。
SDS 会用 buf 字节数据保存数据,对存放在 buf 中的数据都是以二进制的方式来处理,不会对其进行任何过滤、限制、假设,存进去是什么样,读取的时候就是怎么样。
总结
与 C 字符串相比,SDS 具有如下优点:
- $O(1)$复杂度获取字符串长度
- 杜绝缓冲区溢出
- 减少修改字符串时内存重新分配次数
- 二进制安全
- 兼容部分 C 字符串函数