redis学习系列(四)--redis基础SDS的构造

博客写的不少了,但是越写越好奇有些博客为什么写的那么有深度,而自己的博客确是那么的浅,读过有些大牛的redis博客,发现有很多都是实践中的心得,确实比只读书做笔记的来的深的多,但是,但是,但是,公司的redis架构已经成熟了,平时根本没机会去接近redis的问题,也无法得知redis的环境,不是自己项目组,根本了解不了,哎,心累啊。


redis这个系列的博客,权当自己的读书笔记了,怎么办呢?记又麻烦,不记又不行,看完很快忘光,


今天来看下redis结构中的基础结构SDS

struct sdshdr {

    // 记录 buf 数组中已使用字节的数量
    // 等于 SDS 所保存字符串的长度
    int len;

    // 记录 buf 数组中未使用字节的数量
    int free;

    // 字节数组,用于保存字符串
    char buf[];

};
len是已使用的,free是未使用的,buf[]是记录内容的,所以len可以看做事已保存的字符串长度,但是这个SDS的长度却不是len的长度,而是len+free+1(1是结尾"\0")


digraph {    label = "\n 图 2-1    SDS 示例";    rankdir = LR;    node [shape = record];    //    sdshdr [label = "sdshdr | free \n 0 | len \n 5 | <buf> buf"];    buf [label = "{ 'R' | 'e' | 'd' | 'i' | 's' | '\\0' }"];    //    sdshdr:buf -> buf;}

SDS仍然遵循的是C的字符串那一套,最后的\0不在len里面

下例子中拥有5个字节长度的未使用空间

digraph {    label = "\n 图 2-2    带有未使用空间的 SDS 示例";    rankdir = LR;    node [shape = record];    //    sdshdr [label = "sdshdr | free \n 5 | len \n 5 | <buf> buf"];    buf [label = "{ 'R' | 'e' | 'd' | 'i' | 's' | '\\0' | | | | | }"];    //    sdshdr:buf -> buf;}


SDS与一般C语言中的区别

1.获取长度时的区别:

1.1 C语言中获取长度,需要遍历,这就需要消耗不少的性能,时间复杂度是O(N)redis里面肯定不能允许这种操作存在。

1.2 redis中由于使用了len的计数方式,所以获取字符串长度的时间复杂度是O(1)

设置和更新SDS长度的工作是由SDS的API在执行时自动完成的,因此使用SDS无需进行手动修改长度的工作

因此通过SDS而不是C字符串,因此对一个很长的字符串即使反复执行STRLEN命令,也不会对系统造成任何影响。

2.杜绝缓冲区溢出:

c字符串内容易造成缓冲区溢出的问题,但是在redis内,SDS的空间分配策略杜绝了发生缓冲区溢出的可能性,当SDS API需要对SDS进行修改时,API会先检查SDS的空间是否满足修改所需的大小,不满足的话,API会先自动将SDS的空间扩展至执行修改所需的大小,然后才执行实际的修改操作。

redis内有个函数sdscat操作,它能将一个字符串保存到SDS所保存的字符串后面,原有保存的数据如下:

digraph {    label = "\n 图 2-9    sdscat 执行之前的 SDS";    rankdir = LR;    node [shape = record];    //    sdshdr [label = "sdshdr | free \n 0 | len \n 5 | <buf> buf"];    buf [label = "{ 'R' | 'e' | 'd' | 'i' | 's' | '\\0' }"];    //    sdshdr:buf -> buf;}

执行保存之前先扩容,扩容时还额外分配了13的free空间,这个后面再说

digraph {    label = "\n 图 2-10    sdscat 执行之后的 SDS";    rankdir = LR;    node [shape = record];    //    sdshdr [label = "sdshdr | free \n 13 | len \n 13 | <buf> buf"];    buf [label = "{ 'R' | 'e' | 'd' | 'i' | 's' | ' ' | 'C' | 'l' | 'u' | 's' | 't' | 'e' | 'r'| '\\0' | ... }"];    //    sdshdr:buf -> buf;}

分配策略

redis使用了空间预分配惰性空间释放两种策略

内存分配会涉及到复杂的算法,并且可能会执行系统调用,因此通常比较耗时,但是redis所谓nosql数据库,经常使用在速度要求严苛,并且数据被频繁修改的场景,如果每一次执行时都要去重新分配内存空间,肯定会对性能造成影响。


空间预分配

空间预分配使用在SDS的字符串增长操作,当SDS的API对一个SDS进行修改,并且需要对SDS进行空间扩展时,程序不仅需要分配额外的空间,还需要分配额外的未使用空间。

1.如果对SDS进行修改之后,SDS的长度将小于1MB,那么程序会分配和len属性同样大小的未使用空间,这是len和free属性的值相同。也就是上例子中的free和len都是13,那么此时SDS的buf数组的实际长度变成13+13+1=27字节,额外的1字节是“\0”

2.如果对SDS进行修改之后,SDS的长度大于1MB,那么程序会分配1MB未使用的空间,例如,进行修改之后,SDS的len变成30MB,那么程序分配1MB额外的未使用空间,SDS的buf数组的实际长度30MB+1MB+1byte

通过这个策略,redis可以减少连续执行字符串增长操作所需的内存重分配字数,因为下一次如果再增加字符串进来,可以直接使用未使用空间的内存。

从必定N次降低为最多N次

惰性空间释放

惰性空间释放顾名思义用于缩短操作时,当SDS的API需要缩短SDS保存的字符串时,程序不立马使用内存重分配来回收缩短多出来的字节,而是使用free属性将这些字节的数量记录起来,等待下次使用

使用sdstrim,执行之前,len为11

digraph {    label = "\n 图 2-14    执行 sdstrim 之前的 SDS";    rankdir = LR;    node [shape = record];    //    sdshdr [label = "sdshdr | free \n 0 | len \n 11 | <buf> buf"];    buf [label = " { 'X' | 'Y' | 'X' | 'X' | 'Y' | 'a' | 'b' | 'c' | 'X' | 'Y' | 'Y' | '\\0' } "];    //    sdshdr:buf -> buf;}

执行之后,len变成3.缩短了的内存记录在free中

digraph {    label = "\n 图 2-15    执行 sdstrim 之后的 SDS";    rankdir = LR;    node [shape = record];    //    sdshdr [label = "sdshdr | free \n 8 | len \n 3 | <buf> buf"];    buf [label = " { 'a' | 'b' | 'c' | '\\0' | <1> | <2> | <3> | <4> | <5> | <6> | <7> | <8> } "];    //    sdshdr:buf -> buf;}

如果此时再执行新增操作,直接使用free空间内的内存,因为预留出的足够存放此次新增的字符串,当然,redis也提供了API,真正执行SDS未使用空间的释放

digraph {    label = "\n 图 2-16    执行 sdscat 之后的的 SDS";    rankdir = LR;    node [shape = record];    //    sdshdr [label = "sdshdr | free \n 2 | len \n 9 | <buf> buf"];    buf [label = " { 'a' | 'b' | 'c' | ' ' | 'R' | 'e' | 'd' | 'i' | 's' | '\\0' | <1> | <2> } "];    //    sdshdr:buf -> buf;}


表 2-1 C 字符串和 SDS 之间的区别

C 字符串 SDS
获取字符串长度的复杂度为 O(N) 。 获取字符串长度的复杂度为 O(1) 。
API 是不安全的,可能会造成缓冲区溢出。 API 是安全的,不会造成缓冲区溢出。
修改字符串长度 N 次必然需要执行 N 次内存重分配。 修改字符串长度 N 次最多需要执行 N 次内存重分配。
只能保存文本数据。 可以保存文本或者二进制数据。
可以使用所有 <string.h> 库中的函数。 可以使用一部分 <string.h> 库中的函数。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值