介绍
Redis没有使用C字符串(以空字符结尾),而是自己构建了简单动态字符串(Simple Dynamic String,SDS)
的抽象类型,并将SDS用作Redis的默认字符串支持
用处
- 数据库中的字符串值(键值对的键、字符串值、其他类型的字符串部分)
- 缓冲区,AOF模块的AOF缓冲区,客户端状态的输入缓冲区
定义
// sds.h
struct sdsdr{
int len;
int free;
char buf[];
}
- len:字符串长度
- free:未使用空间
- buf:保存字符的数组,字节数组,后面会提到
字符串长度不包括结尾空字符,因此buf的长度应该为len+free+1
sds遵循的c字符串空字符结尾的好处是可以直接宠用c字符串函数库里的函数
SDS与C字符串的区别
1. 常数复杂度获取字符串长度
因为字符串的长度直接由len记录(o(1)),所以不必遍历获取字符串长度(o(n)),更新字符串长度的工作由SDS的API在执行时自动完成。
2. 杜绝缓冲区溢出
因为C字符串不记录自身长度,所以容易造成缓冲区溢出
比如由一段内存连续排着两个字符串(‘H’,‘i’,’\0’,‘W’,‘o’,‘r’,‘l’,‘d’,’\0’)
此时将Hi改成Hello则会变成(‘H’,‘e’,‘l’,‘l’,‘o’,‘r’,‘l’,‘d’,’\0’)
而SDS的API在对SDS进行修改的时候,会先检查SDS空间是否满足需求,不满足则SDS会自动扩展,再执行修改操作
3. 减少修改字符串带来的内存重分配次数
由于C字符串不保存自身长度,N个字符的字符串由N+1长度的字符数组实现(有一个结尾空字符),字符串长度和数组长度关联。每次增长或缩短字符串,都需要进行一次内存重分配,不然就会缓冲区溢出(增长)或者内存泄漏(缩短)。而且内存重分配涉及复杂算法,并且可能需要执行系统调用,所以比较耗时(Redis作为数据库对速度要求严苛,数据频繁修改)。
SDS通过未使用空间解除了字符串长度和数组长度的关联。SDS通过未使用空间实现了空间预分配
和惰性空间释放
两种优化策略
空间预分配
空间预分配用于优化SDS的字符串增长操作,当SDS需要扩展空间的时候,不仅分配修改所需的空间,也分配额外未使用空间,而分配额外未使用空间有两种公式
- 如果修改之后,len<1MB,则分配和len相同的未使用空间,len将和free的值相同。假设“Redis”字符串len=5,free=0 ,数组长度为6,在Redis后添加“ Cluster”(前面有空格),会给两部分都分配13字节的空间,len将会更新为13,而free也会相应更新为13,数组长度为13+13+1=27
- 如果修改之后,len>=1MB,那么程序则会分配1MB的未使用空间
惰性空间释放
惰性空间释放用于优化SDS的字符串缩短操作,当SDS的API需要缩短SDS保存的字符串时,程序并不会立即使用内存重分配来回收缩短后多出来的字节,而是更新free属性,将这些字节数量记录起来,等待将来使用
通过惰性空间释放,SDS避免了缩短字符串所需的内存重分配操作,并未将来可能的增长操作提供优化
当然,SDS也提供了相应的API,让我们可以在有需要的时候,真正地释放SDS未使用空间,所以不必担心内存浪费
4. 二进制安全
c字符串必须符合某种编码(如ASCII),并且在字符串之间不能有空字符,不然会认为在这里结束,这样子,c字符串就只能保存文本数据,不能保存二进制数据。SDS的API都会以处理二进制的方式处理SDS存放在buf数组里,数据写入什么样,读取就什么样。
所以说SDS的buf是字节数组,redis用它来存二进制数据。(虽然sds使用了空字符串,但它是用len来判断字符串结束)
5. 兼容部分C字符串函数
sds可以用来保存二进制数据,但它也遵循了空字符的惯例,所以当它保存文本数据的时候,可以使用一部分C字符串的函数.
通过以空字符串结尾的惯例, SDS可以在有需要时重用<string.h>函数库,避免不必要的代码重复
6. 总结
C字符串 | SDS |
---|---|
获取字符串长度时间复杂度o(N) | 获取字符串长度时间复杂度o(1) |
API是不安全的,可能造成缓冲区溢出 | API安全,不会造成缓冲区溢出 |
修改字符串长度n次必然需要执行n次内存重分配 | 修改字符串长度n次,最多n次内存重分配 |
只能保存文本数据 | 可以保存文本数据和二进制数据 |
可以使用全部<string.h>库的函数 | 可以使用部分<string.h>库的函数 |
SDS API
函数 | 作用 | 时间复杂度 |
---|---|---|
sdsnew | 创建给定C字符串的SDS | o(N), N是C字符串长度 |
sdsempty | 创建空SDS | o(1) |
sdsfree | 释放给定的SDS | o(N),N是SDS长度 |
sdslen | 返回SDS的已使用空间字节数 | o(1) |
sdsavail | 返回SDS未使用空间字节数 | o(1) |
sdsdup | 创建一个给定SDS的副本 | o(N),N是SDS长度 |
sdsclear | 清空SDS保存的字符串内容 | o(1) |
sdscat | 将C字符串拼接到SDS后面 | o(N), N是C字符串长度 |
sdscatsds | 将SDS拼接到SDS后面 | o(N),N是被拼接SDS长度 |
sdscpy | 将给定C字符串赋值到SDS里面,覆盖原来的字符串 | o(N), N是C字符串长度 |
sdsgrowzero | 用空字符串将SDS扩展至给定长度 | o(N),N是扩展新增长度 |
sdsrange | 保留SDS给定区间内的数据,不在区间内的数据会被覆盖或清除 | o(N),N是被保留数据长度 |
sdstrim | 接受一个SDS和一个C字符串,将SDS中C字符串出现过的字符移除 | o(N2), N是C字符串长度 |
sdscmp | 对比两个SDS是否相等 | o(N),N是两个SDS中较短的长度 |
参考《Redis设计与实现》