redis高性能SDS字符串实现
在C语言中,采用长度为N+1的字符数组来存储一个长度为N的字符串,最后一个字符是空字符\0,表示字符串的结尾。
在C语言中,增加或者减少字符串的长度都涉及内存空间的重新分配,这是很耗时的,对于一个作为数据库的应用redis来说,直接使用这种方式会严重影响性能。C语言对字符串的操作过程如下:
- 如果增加字符串的长度的话,在操作之前需要通过重新分配内存来扩展存储该字符串底层数组的长度,否则会发生缓冲区溢出。
- 如果减少字符串长度的话,在操作之前同样需要重新分配内存释放多余的内存空间,不重新分配内存虽然不会直接造成程序崩溃,但是当这种操作多了以后,就会有大量的空闲的内存空间而导致内存泄漏问题。
SDS存储的结构体
struct sdshdr{
int len; //buf数组中已经使用的字节的数量,也就是SDS字符串长度
int free; //buf数组中未使用的字节的数量
char buf[]; //字节数组,字符串就保存在这里面
};
redis通过定义结构体的方式,扩展了C语言底层字符串的缺点,字符串长度的获取时间复杂度从原来的O(N)变成了O(1),另一方面也可以通过free的动态改变减少内存的分配。需要强调一点的是buf数组不是存储的字符,而是二进制数组,因为C语言字符串中间是不能出现空字符的,而二进制数据中间很有可能会有空字符,所以C语言是二进制不安全的,而redis又是二进制安全,为了存储多种类型的数据,redis就直接把所有数据当作二进制来存储,这样就可以存储媒体文件和字符串,所以SDS虽然叫简单动态字符串,但是它可不只是用来保存字符串。
redis的内存分配策略如下:
- 当SDS的len属性长度小于1MB时,redis会分配和len相同长度的free空间。至于为什么这样分配呢,我觉得这个有点像一种惯性预测。举个例子,比如一个乞丐向你要10块钱,如果让你预测下一个乞丐会让你要多少,这个时候我没有其他的依据,当然就只能根据上一个乞丐的行为来推测下一个乞丐会让我要10块啦~放到redis里面,上次用了len长度的空间,那么下次程序可能也会用len长度的空间,所以redis就为你预分配这么多的空间。
- 但是当SDS的len属性长度大于1MB时,这个时候我在根据这种惯性预测来分配的话就有点得不偿失了,比如修改后的SDS长度为100MB,那我也傻乎乎的给你分配100MB空闲内存等你用么?万一你下次用不了这么多了,对于内存而言就亏大发了。所以,redis是将1MB设为一个风险值,没过风险值你用多少我就给你多少,过了的话那这个风险值就是我能给你临界值,感觉父母给零花钱也是酱紫。。至于为什么是1MB,这个问题就和redis使用的场景有关了。
reids的内存回收策略如下:
- redis的内存回收采用惰性回收,即你把字符串变短了,那么多余的内存空间我先不还给操作系统,先留着,万一马上又要被使用呢。短暂的持有资源,既可以充分利用资源,也可以不浪费资源。这是一种很优秀的思想。
综上所述,redis实现的高性能字符串的结果就把N次字符串操作必会发生N次内存重新分配变为人品最差时最多发生N次重新分配。