Redis中的字符串使用的是自己的实现,解决了使用原生字符串的一些弊端
在源码中定义为sds.h
1.0
1.0 的定义是这样的
1 | struct sdshdr { |
通过两个字段标记buf中空闲的大小和已经有的字符串的长度
获得了以下的好处
- 使得strlen的时间复杂度变成了O(1)
- 避免缓冲区溢出问题,使用自定义的strcat等函数
- 空间预分配,当需要对SDS进行扩展的时候,如果空间不足,再分配时会依据一些策略预分配一些空间
- 如果对SDS修改后len < 1MB,那么将预分配len大小的空间,就是free = len
- 如果对SDS修改后len >= 1MB, 那么将预分配1MB大小的空间, 就是free = 1MB
有分配就有收回
关于收回有两个函数sdstrim
和sdsRemoveFreeSpace
第一个并没有回收buf数组,而是把多余的空间的大小写到了free中
第二个是进行了回收buf数组的操作
我再去看sdsRemoveFreeSpace的调用,发现其实调用的并不多
可能是Redis的场景中,直接delete的较多,进行remove并不多
而且Redis提供的操作中也只有append操作可以对字符串进行增加
具体哪些会调用remove,TODO一下,等后面看到了再说
我在看书的时候顺便去翻了下源码
发现3.2之后进行了一番的升级
变得略复杂
为什么要进行了升级呢,其实SDS的定义也不是毫无缺点
看这个issue就知道了
https://github.com/antirez/redis/issues/757
问题就在len和free的使用类型上,使用的是unsigned int
- unsigned int,也就是32位,最多只能记录4GB大小,超过4G的大小将无法使用
- 内存损耗,如果字符串都非常短,可能16位就能记录,那么32位就损耗了2位
(其实如果我倒觉得这两个不是什么问题,果然大佬的思想就是不一样)
但是既然问题提出来了就需要解决
简单的换len和free的类型吗
这个不行,因为你仔细观察上面两个缺点,发现他们其实是有点互斥的成分在里面
一个嫌弃是unsigned int过小,一个是嫌弃unsigned int过大
然后这个PR就提出了自适应的sdshdr
https://github.com/antirez/redis/pull/2509
也就是SDS2.0的实现
2.0
1 | struct __attribute__ ((__packed__)) sdshdr5 { |
这是2.0的struct定义
根据长度分为了5种,分别是8位,16位,32位,64位
每个定义中使用了一个char类型的flags标记当前的struct是什么类型
再注意看还有一个__attribute__ ((__packed__))
的定义
这个我也不知道是啥,所以特地去查了一下
结果是GCC的编译器的一个标记选项,使用紧凑结构编译
正常情况下的struct会使用内存对齐的
比如这个1
2
3
4
5
6
7
8struct stu {
char ch;
int v;
};
int main() {
printf("%d\n", (int) sizeof(struct stu));
return 0;
}
结果在我的64位机器上输出的是8
虽然char是1位,int是4位,但是因为对齐的原因,ch和v之间有三位是不用的,属于padding位
所以整个struct的占用大小是8
但是如果加上__attribute__ ((__packed__))
选项呢1
2
3
4
5
6
7
8struct __attribute__ ((packed)) stu {
char ch;
int v;
};
int main() {
printf("%d\n", (int) sizeof(struct stu));
return 0;
}
你会发现输出的是5
这个关键词看起来是极好的,但是其实也有弊端
既然节约了空间,那么在时间上肯定是有损耗的
正常我们读取v数据在32位的数据总线上,读取一次就够了,因为地址总线按照32位进行寻址的
但是如果是上面这种,因为v分布在两个32位的地址中,数据总线可能需要进行两个读取才能把v的值拿到
说回SDS,既然分为了五种类型,那么在进行一些操作的时候就要分情况讨论了
因为在操作SDS的时候,统一的全部换成了地址进行操作1
typedef char *sds;
之前的定义,len肯定是在前4位的,但是现在不知道了,所以要先找出type,看是哪一种类型
1 | static inline size_t sdslen(const sds s) { |
比如计算长度这个,得先拿出flags,判断下属于哪个类型的SDS,然后再对长度所在的地址进行取值