Redis并不是直接使用C的原生字符串,而是对char*进行了简单的包装,但是经过包装后会避免很多缺陷。
C原生字符串的缺点:
使用API时不安全:比如调用strcat()进行字符串拼接时,strcat(str1, “extend”)
原数据:
经过拼接后:
str2中原来的数据被破坏,因为C语言在执行这些API时不进行溢出检查,只是一股脑地把数据填到后边。
获取字符串长度,时间复杂度为O(n), C语言如何获取一个char的长度呢?当然是用循环判断到’\0’, 如果Redis直接采用char, 那么在获取字符串长度时就会影响性能。
不能保证二进制安全:, 一个字符串中只要出现’\0’, 就会被截取,如:
那么调用API处理这个字符串时,只会处理"string", 而忽略后面的"end"。
修改字符串需要重新分配空间,C原生字符串占用的空间永远都N+1(字符串长度+’\0’), 这就使得只要增加或缩小字符串,都要重新申请或释放空间,这些过程一般都会涉及系统调用,陷入内核,也影响了性能。
下面看Redis中的字符串是如何避免上面的问题的,刚才说过,Redis中的字符串是对char*的包装,定义如下:
src/sds.h
Redis使用sds(simple dynamic string: 简单动态字符串来作为默认字符串),
从源码中可以看到,只是增加了两个属性,len, free, 用来记录已使用空间和剩余空间,三个成员作用可从下图中看出来:
当存放字符串时:
下面简单分析一下,这个东西是如何避免上述C原生字符串的问题的,
- 使用len成员可以直接获取字符串的长度,时间复杂度为O(1), 如下:
- 二进制安全,不会出现字符串中间的’\0’,把字符串截断的情况(使用len成员来作为字符串长度),比如输出吧,C的原生字符串遇到中间的’\0’会截断,但是Redis中的字符串不会,一直输出len个字符为止,而不是按’\0’来判定,而是根据len成员来判定。
- 使用API时安全,比如上面的strcat拼接引起溢出的情况, Redis可先检查free成员,如果没有剩余空间,则先申请空间,来避免直接覆盖引起溢出。
- 既然有了len和free成员就比较容易控制申请和释放空间的次数。比如申请的时候,多申请一些,来避免每次都申请; 释放的时候,先不真正释放空间,而是暂时用free成员记录,万一等会又需要申请空间,就可以直接使用这些空间。
可以看到248行,新空间的大小,不仅仅是刚才容纳新内容的长度,而是其两倍,比如原来长度为3, 新串长度为5,接起来长度为8, 那么就申请16.(8×2),251行是另一种情况,也是多申请了SDS_MAX_PREALLOC个字节空间。
由于Redis中字符串是C字符串的包装,还可以使用C中的string.h中的一些函数(处理buf成员,而且buf指向的字符串仍然以’\0’结尾)。
参考:
《Redis 设计与实现》
Redis中文注释源码:https://github.com/huangz1990/redis-3.0-annotated