最近看了Redis的SDS动态字符串的一些知识,总结一下。本篇只写SDS的一些数据结构相关的,不涉及其他内容。
大家都知道Redis底层是用C写的,但是Redis并没有直接使用C的字符串来进行保存(以空字符结尾的字符数组),而是自己构建了一种简单的动态字符串SDS,默认为Redis的字符串表示。
本篇文章用来说明SDS和C字符串不同之处,为什么使用SDS而不是直接使用C字符串。
简单演示字符串的键值都是由SDS实现
如果客户端执行以上命令
- 键值对的键是一个字符串对象,底层实现是一个保存着字符串"msg"的SDS。
- 键值对的值时一个字符串对象,底层实现是一个保存着字符串"helloworld"的SDS。
简单介绍一下SDS的结构
- free值的值为0,表示这个SDS没有分配任何未使用空间
- len属性的值为5,表示SDS保存了一个长度为5字节的字符串
- buf属性是一个char类型的字符数组,最后一个则保存了空字符‘\0’
- SDS遵循了C空字符结尾的惯例,但是该空字符不计算在len属性上,为空字符额外给1字节的空间以及添加空字符在字符结尾等操作,都是SDS函数自己调用完成的,对于用户来说是透明的。遵循C空字符结尾的惯例的好处是可以重用一些C字符串函数库的函数操作。
SDS与C字符串的区别
1.获取字符串长度的时间复杂度
C字符串并不记录自身长度的信息,所以如果要获取字符串的长度,必须从头到尾遍历一遍字符串的长度,直到遇到空字符结尾为止。这个操作的复杂度为O(N)。和C不一样的是,SDS在len属性上记录了字符本身长度,其复杂度是O(1)。设置和更新SDS的len属性都是SDS的API自动完成的,无需手动操作。Redis获取字符串长度的复杂度从O(N)降底到O(1),这确保了获取字符串长度的工作不会成为Redis的性能瓶颈。
2. 杜绝缓冲区溢出
C字符串不记录自身长度,假如执行strcat函数进行拼接字符的时候,没有为之前的字符串分配足够的空间,就会导致数据的溢出。与C不同的是,SDS的空间分配策略完全杜绝了发生缓冲区溢出的可能性,当SDS API要修改SDS时,API会先检查SDS是否满足修改所需的要求,不满足的话会自动的扩展至执行修改所需的大小,所以不会出现缓冲区溢出的问题。
3.减少修改字符串时带来的内存重分配次数
在SDS中,数组里面可以包含未使用的字节,记录在free属性中,因为如果每次修改字符串长度要执行一次内存重新分配的话,光是分配这一块时间都会很浪费,也会对性能有一定的影响。
4.二进制的安全
C字符串里面不能包含空字符,不然最先被程序读入空字符会被误认为字符串结尾,所以C字符串只能保存文本数据,而不能保存像图片、音频、视频、压缩文件等这样的二进制数据。而SDS的API是二进制安全的,所有SDS API都会以处理二进制的方式来处理SDS存放在buf数组里面的数据。这也是我们SDS的buf属性成为字节数组的原因,Redis不是用该数组来保存字符,而是用它来保存一系列的二进制数据的。
5.兼容部分C字符串的函数
比如说是<string.h>的strcasetemp函数之类的。这里就不一一列举了。。。。通过遵循C字符串以空字符结尾的惯例,SDS有需要时可以重用<string.h>函数库。从而避免了不必要的代码重复。
贴张图
SDS对未使用空间的两种空间策略
1. 空间预分配
- 空间预分配用于优化SDS字符串增长操作,当SDS的API对一个SDS进行修改并空间扩展时,程序不仅会为SDS分配所需要的空间,还会给它分配额外的未使用空间。
- 对SDS修改后的len值小于1MB,那么程序会分配给len属性一样大小的未使用空间,这时SDS的len属性和free属性的值相同。
- 对SDS修改后的len值大于等于1MB,那么程序会分配1MB的未使用空间。
- 通过这种空间预分配,Redis可以减少连续字符串增长操作所需要的内存重分配次数。将连续增长N次字符串所需的内存重分配必定为N次降低为最多为N次。
2.惰性空间释放
- 用于优化SDS的字符串缩短操作,当SDS的API需要缩短SDS保存的字符串时,程序并不会立即使用内存重分配来回收缩短后多出来的字节,而是会将这些字节的数量记录到free属性中,并等待将来使用。
- SDS也提供相应的API,当我们有需要时可以释放SDS的未使用空间,这样就不会导致这种策略会造成内存浪费。