-
引言
redis在我日常工作中使用的频率相当高,每个项目基本都会用到。在redis的使用过程中,字符串使用的频率也是相当的高,所以有必要学习redis的字符串的相关知识。文中涉及的代码为redis3.0版本 -
简单了解SDS(Simple Dynamic String 简单动态字符串)
- redis使用sds进行字符串的表示,没有使用C默认的
char*
类型,char*
的功能单一,抽象层次低,不能高效的支持一些redis的常用操作,如追加和长度计算。 - SDS数据结构
struct sdshdr { // 记录buf已使用的字节数量 // 即已保存的字符串的长度 int len; // buf 中剩余可用的字节数量 int free; // 字节数组,用于保存字符串 char buf[]; };
- SDS示例
free:0,表示sds没有未使用空间 len:5,表示保存的字符串长度是5 buf:char数据,保存了R、e、d、i、s,buf其实保存了5个内容字符,还有一个\0结束字符, 但是\0不会被记录长度到len当中
4.结束符疑问
为什么还要浪费1个字节空间去记录一个\0结束字符呢?
因为C字符串是以空字符\0结尾的,sds遵循C 字符串以空字符结尾的惯例,并且不会把\0记录到len的长度当中,在分配空间时会额为分配1个字节去记录该字符,整个的操作都是sds的函数自动完成的,使用者无需关心。
使用空字符\0结尾的好处是,可以使用C字符串函数库里面的函数 - redis使用sds进行字符串的表示,没有使用C默认的
-
SDS和C字符串
3.1.C字符串
C字符串的结构如上所示,C字符串不能满足Redis对字符串的安全性、效率以及功能方面的需求,所以redis才会使用sds
3.2. C字符串的问题- 获取字符串长度,获取一个C字符串的长度,程序必须遍历整个字符串,直到遇到结尾的空字符\0为止,时间复杂度为O(n)
- 缓冲区溢出,如果存在两个字符串s1和s2,两个字符串是紧邻的如下图所示,其中s1内容为Redis,s2内容为MongoDB,内存结构图如下所示:
此时对s1进行append操作,使用如下函数:char *strcat(char *dest, const char *src); 执行: strcat(s1, " Cluster");
但是append前并没有对s1分配足够的内存空间,就会导致s1的数据溢出到s2,导致s2的内容被修改。如下图所示:
- 内存频繁分配和回收内存,当对字符串频繁的修改时,C字符串需要频繁的分配和回收内存。
- 二进制安全 ,C 字符串中的字符必须符合某种编码(比如 ASCII), 并且除了字符串的末尾之外, 字符串里面不能包含空字符, 否则最先被程序读入的空字符将被误认为是字符串结尾。举个栗子:
上图的字符串使用了空字符分隔,此时C字符串所用的函数只能识别其中的 “Reids”,会忽略后面的"Cluster"
3.3 SDS如何解决这些问题
-
获取字符串的长度,在SDS的结构体当中使用了len属性记录字符串内容的长度,sds获取长度的代码如下:
/* * 返回 sds 实际保存的字符串的长度 * * T = O(1) */ static inline size_t sdslen(const sds s) { struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr))); return sh->len; }
-
杜绝内存溢出,当 sds api 需要对 sds 进行修改时, api会先检查 sds的空间是否满足修改所需的要求, 如果不满足的话, api会自动将 sds 的空间扩展至执行修改所需的大小, 然后才执行实际的修改操作。
-
减少修改字符串时带来的内存分配次数,C字符串修改字符串需要重新分配内存,否则会导致内存溢出,SDS采取空间预分配和惰性空间释放方法从而较少内存分配次数
空间预分配:- sds api对sds修改时,如果此时需要对sds的空间进行扩展,sds不仅会分配当前操作所需要的空间,还会为sds分配额外的未使用空间,这样redis在后续执行字符串增长操作到时候可以减少内存重新分配的次数。
- 对sds修改后,如果len小于1M,则程序分配的和len相同的大小未使用空间,也就说此时len和free值是相等的。举个栗子:对sds的修改,sds的len变成了13字节,程序会分配13字节的未使用空间,则sds的buf数组的实际长度为13+13+1 = 27字节,即len+free+空字符=27字节
- 对sds修改后,如果len大于1MB,则程序会分配1MB的未使用空间,举个栗子:sds修改后为3MB,那么程序会分配1MB的未使用空间,则buf数组的实际长度为3MB+1MB+1byte
- 通过这种预分配策略, sds 将连续增长 N 次字符串所需的内存重分配次数从必定 N 次降低为最多 N 次。(因为free的空间有可能不满足当前修改操作的所需空,所以是降低为最多N次)
惰性空间释放:
- sds的api对sds的缩短操作时,程序不会立即回收内存,而是使用free属性将这些需要回收字节的数量记录起来,等待将来使用。字符串”XYXXYabcXYY”移除所有的“X”和“Y”,过程如下图所示。
如上图所示,sds并没有释放多出来的 8 字节空间, 而是将这 8 字节空间作为未使用空间保留在了 sds里面, 如果将来要对 sds进行增长操作的话, 这些未使用空间就可能会派上用场
-
二进制安全,sds的api都是二进制安全的,所有 SDS API 都会以处理二进制的方式来处理 SDS 存放在 buf 数组里的数据, 程序不会对其中的数据做任何限制、过滤、或者假设 —— 数据在写入时是什么样的, 它被读取时就是什么样。
-
总结
sds比起c字符串有以下优点:- 常数复杂度获取字符串长度
- 杜绝缓冲区溢出
- 减少修改字符串长度时所需的内存重分配次数
- 二进制安全
- 兼容部分 C 字符串函数
-
参考资料
<<Redis设计与实现>>
Redis学习笔记-简单动态字符串SDS
最新推荐文章于 2024-10-18 13:42:20 发布