1. sds基本结构
1 | |
注意:这里的buf是不占空间的,比如下面的test,求sizeof是0,后面指针的运算都有这个前提在里面
1 | typedef struct test{ |
首先sds在redis里面直接被typedef成了char,也就是说sds就是char,并且在字符串结尾还会追加\0,在c语言里面,默认的字符串就是这样定义的,所以redis为了能直接复用一些库函数,把sds定义成了char*一样的规则。
但是sds是支持预先分配大小,并且当空间没用完的时候进行原地的扩容。于是,redis在sds的前面定义了一个head,但是实际暴露的是字符串的起始位置。
2. 三种编码
2.1 整数编码OBJ_ENCODING_INT
redis中用的最多的应该就是sds了,最基础的数据结构,下面看下这个sds是如何实现的。
上一节讲到redisObject,数据结构如下:
1 | struct redisObject { |
redis的string为了优化节省内存,当string保存的是数字的时候,是直接用ptr存的,相比ptr指向一个sds减少了一次额外的内存分配,这是string的OBJ_ENCODING_INT编码方式。
2.2 紧凑内存编码OBJ_ENCODING_EMBSTR
为了更好的命中cpu缓存,redis的字符串有一种紧凑的编码格式,redisObject和sds一起分配内存,减少了ptr的寻址过程,优点就是有更好的数据局部性,提升cpu的cache命中。
2.3 原始编码OBJ_ENCODING_RAW
redis的string还有一个常规的编码,和紧凑编码唯一区别就是ptr指向的内存不是在ptr指针之后的,而且可能在任意的内存地址。
3. 创建sds
1 | sds _sdsnewlen(const void *init, size_t initlen, int trymalloc) { |
所有的创建字符串都会走到这里,大概逻辑如下
- 根据长度选择合适的sds头部
- 分配sds头部+字符串长度+1的内存,初始化头部信息
- 如果需要根据另外一个字符串初始化,进行内存拷贝(不是字符串拷贝),内存拷贝效率更高
- 最后添加\0
- 返回buf处的地址(字符串的起始位置)
4. 字符串的扩容
redis的字符串可以预留空间,也可以进行扩容,函数是_sdsMakeRoomFor
1 | /* |
sds的扩容,大概有以下几点
- 新长度小于1024*1024,会进行二倍扩容
- 大于10241020,会额外分配10241020作为冗余空间
- 检查sds的头部需不需要重新分配
- 需要重新分配的话,重新申请一块内存,把数据拷贝过去,因为原来的长度字段已经装不下新的长度了,这就需要把数据全部往后挪动
- 不需要重新分配head,就使用realloc进行扩容内存