当前位置:首页 > 问答 > 正文

深入拆解Redis里那个SDS到底是啥玩意儿,源码细节慢慢看

直接整合自知乎高赞回答《深入拆解Redis里那个SDS到底是啥玩意儿,源码细节慢慢看》的核心讲解,以口语化方式呈现)

Redis为啥要自己搞个SDS(Simple Dynamic String)?说白了,就是嫌C语言自带的那个char*字符串太“原始”、太“笨”了,干起活来效率低还不安全,C语言的字符串就是个字符数组,靠个'\0'字符标结束,你想知道这个字符串多长?对不起,你得一个个字符数过去,数到'\0'为止,这个操作的时间复杂度是O(n),要是字符串老长了,这得多慢啊!Redis是数据库,经常要处理海量的键值对,键和值很多都是字符串,这种性能开销根本受不了。

Redis自己设计了一个更聪明的字符串结构,就是SDS,它到底长啥样呢?(来源:源码sds.h)简单说,它就是个“结构体”,里面包着好几样东西,咱们可以把它想象成一个小分队,这个小分队不仅有“干活的主力部队”(就是存真正字符的那块内存),还有个“指挥部”,指挥部里记着关键信息。

指挥部里记了啥?(来源:源码sds.h中的sdshdr结构)主要记三样宝贝:

深入拆解Redis里那个SDS到底是啥玩意儿,源码细节慢慢看

  1. len:记录这个字符串当前的实际长度是多少,比如字符串是"hello",len就是5,有了它,想知道字符串多长,直接看一眼这个len就行了,时间复杂度是O(1),瞬间完成,爽不爽?
  2. free:记录分配给这个字符串的内存空间,除了已经用的,还剩下多少空闲的,比如可能分配了10个字符的空间,但只用了5个存了"hello",那free就是5,这个free空间就是预分配好的“备用仓库”。
  3. 还有一个flags,用来标识不同类型的SDS(比如区分长度用8位字节存还是用16位、32位等存,是为了更省内存),这个咱们可以先不深究。

你看,有了len和free,SDS就比C字符串厉害多了,厉害在哪儿呢?

第一,长度获取超快,O(1)复杂度,再也不需要遍历了。

第二,杜绝了缓冲区溢出,C字符串拼接时,如果你忘了检查目标空间够不够,很容易就把相邻的内存给覆盖了,非常危险,SDS就没这问题,它在做拼接操作(比如sdscat)前,(来源:源码sds.c中的sdscatlen函数)会先看看free够不够,如果不够,它会自己先扩容,确保有足够空间了再拼,安全又省心。

深入拆解Redis里那个SDS到底是啥玩意儿,源码细节慢慢看

第三,减少了内存重分配次数,这是SDS的一个核心优化策略,叫“空间预分配”和“惰性空间释放”。

  • 空间预分配:(来源:源码sds.c中的sdsMakeRoomFor函数)当SDS需要扩容时,它不只是仅仅分配刚好够用的新空间,它会多分配一点,具体多多少?有个规则:如果扩容后新的len小于1MB,那就多分配和新len一样大小的free空间,比如原来"hello" len=5,现在要拼成"hello world" len=11,那么SDS可能会分配 11(新len) + 11(预分配free) + 1(存'\0') = 23个字节的空间,这样下次如果再拼接一个短字符串,可能直接用free空间就行了,不用再申请内存,如果新len已经大于等于1MB了,那就固定多分配1MB的free空间,避免浪费,这个策略大大减少了内存重分配这种耗时操作。
  • 惰性空间释放:(来源:源码sds.c中的sdstrim、sdsrange等函数)当SDS字符串缩短时(比如把"hello world"截断成"hello"),它并不会立即把多出来的内存还给操作系统,而只是把free的值增加(比如从0变成6),把len减小,这样这些多出来的空间就留在SDS内部备用,万一以后这个字符串又变长了,就能直接用上,避免了缩了又涨的情况下反复分配内存。

第四,二进制安全,C字符串因为靠'\0'判断结束,所以里面不能包含'\0'字符,否则会被误认为是字符串结束了,这就只能存文本,不能存图片、压缩数据这种二进制数据,SDS不一样,它靠len来判断长度,根本不在乎你字符串里面有没有'\0',它能把任何数据都原原本本地存进去、读出来,所以Redis才能用SDS来存各种类型的数据。

SDS还“心机”地保留了在字符串末尾自动加个'\0'的习惯(来源:源码sds.h中关于\0终止符的注释),这样一部分遵循C字符串惯例的函数(比如printf)也能直接用了,算是兼容了一下老前辈。

所以你看,Redis这个SDS,看起来简单,但处处都是为高性能、高安全性考虑的精巧设计,它通过一个带元信息的结构体头,把C字符串的很多短板都给补上了,是Redis又快又稳的重要基石。