Redis底层数据结构-SDS

1、简单动态字符串

​ redis是用C语言写的,而redis语言没有直接使用C语言的字符串(以\0结尾的的字符数组),而是自己构建一种名为SDS(简称)的抽象类型。

​ 在redis内如果仅仅需要字符串字面量,无需对字符串进行修改的时候,redis会使用C字符串进行表示(打印日志),当不仅仅需要字面量,redis会用SDS进行表示。

例:

redis> SET msg "Hello World!"

以上例子实际保存着msgHello World!的SDS。

1.1 SDS的定义

每个sds.h/sdshdr结构表示一个SDS值:

struct sdshdr{
  //记录buf数组中已使用字节的数量
  //等于SDS所保存字符串的长度
  int len;
  
  //记录buf数组中未使用字节的数量
  int free;
  
  //字节数组,用于保存字符串
  char buf[];
}

在这里插入图片描述

图2-1展示了一个长度为5的SDS

  • free为0,表示这个SDS没有分配任何未使用的空间
  • len为5,表示这个SDS保存着一个五子节长度的字符串
  • buf是一个char类型的数组,实际保存字符串的内容,以\0结尾

SDS遵循C字符串以空字符结尾的惯例,保存空子符的1字节空间不计算在SDS的len属性里面,遵循这一惯例的好处是可以治直接重用一部分C字符串函数库里面的函数。例:printf("%s", s->buf)

1.2、SDS与C字符串的区别

C语言使用长度为N+1的字符数组来表示长度为N的字符串,并且字符数组的最后一个元素总是\0

1.2.1、常数复杂度获取字符串的长度

C字符串不记录自身长度信息,所以获取C字符串的长度,必须遍历整个字符串,对遇到的每个字符串进行计数,知道遇到代表字符串结尾的空字符为止,这个操作复杂度为O(N)

因为SDS在len属性中记录了SDS本身的长度,所以获取一个SDS长度的复杂度仅为O(1)。

PS:O(N)表示时间复杂度,表示的是线性阶,随着问题规模N的不断增大,上述时间复杂度不断增大,算法执行效率越低。

1.2.2 杜绝缓冲区溢出

C字符串会有缓冲区溢出问题:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m142cTiX-1658995876246)(/Users/hanshuai/Documents/note/redis/readImage/缓冲区溢出.png)]

如图2-7,内存中有两个连续的字符串,假定程序员为Redis字符串进行拼接操作,且没有分配足够的空间,那么拼接的字符串就会溢出到s2所在的空间中,导致s2被意外修改(见下图)。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yWiyu0np-1658995876246)(/Users/hanshuai/Documents/note/redis/readImage/溢出.png)]

SDS的空间分配策略杜绝了缓冲区溢出的可能性,当API需要对SDS进行修改的时候,API会先检查SDS的空间是否满足修改所需的需求(free的大小是否足够),不满字API会自动将SDS的空间扩展至执行修改所需的大小,然后才执行实际的修改操作(并同时分配未使用的空间)。

PS:缓冲区:从磁盘读取数据放在缓冲区,计算机从缓冲区读取数据,减少磁盘读写次数。

1.2.3 减少修改字符串时带来的内存重分配次数

C字符串每次增加或缩短一次,都会进行一次内存重分配操作:

增长字符串:先扩展底层数组空间大小,如果没做则造成内存溢出

缩短字符串:释放掉缩短的那部分空间,如果没做则造成内存泄漏

内存重分配涉及复杂算法,是一个比较耗时的操作

  • 一般程序中,修改字符串的情况不太常出现,每次修改字符串重新分配一次内存也是可以接受的。
  • redis作为数据库,对于速度要求严苛,数据被频繁修改的场合,如果每次修改字符串都会执行一次内存重新分配的话,那么光是内存分配就会占去一大部分时间,如果修改频繁,可能还会对性能造成影响。

为了避免C字符串的缺陷,SDS解除了字符串长度和底层数组长度之间的关联,在SDS中,数组可以包含未使用的字节(free属性)。

通过未使用空间,SDS实现了空间预分配和惰性空间释放两种优化策略。

1、空间预分配

优化字符串增长操作

对SDS进行增加操作的时候:

  • 如果SDS的长度小于1MB,那么程序将分配和len属性同样大小的未使用空间,freelen属性值相等。
  • 如果SDS的长度大于1MB,那么程序将分配1MB未使用的空间。

通过空间预分配策略,Redis可以减少连续执行字符串增长操作所需的内存重分配次数。

在扩展SDS空间之前,会先检查未使用空间是否足够,足够则直接使用未使用空间,而无需执行内存重分配。

通过这种预分配策略,SDS将连续增长N次字符串所需的内存重分配次数从必定N次降低为最多N次。

2、惰性空间释放

优化字符串缩短操作

对SDS进行缩短操作的时候,程序不立即回收缩短之后多出来的字节,而是使用free属性进行记录,并等待将来的使用。

SDS也提供相应API,在有需要的时候,真正的释放SDS未使用的空间,所以不必担心惰性释放造成内存浪费。

通过惰性空间释放,SDS避免了缩短字符串时所需的内存重分配操作,并为将来可能有的增长提供了优化。

1.2.4 二进制安全

C字符串必须符合某种编码(比如ASCII),并且除字符串结尾,其他位置不能出现空字符,否则最先被程序读入的空字符将被误认为是字符串的结尾,这些限制使得C字符串只能保存文本数据,而不能保存图片、音频、视频压缩文件这样的二进制数据。

SDS的API二进制都是安全的,数据在写入是什么样的,读取就是什么样的,通过buf数组进行保存一系列二进制数据,同时是使用len属性值而不是使用空字符来判断字符串是否结束的。

所以SDS是二进制安全的,不仅仅可以保存文本数据,还可以保存任意格式的二进制数据。

1.2.5 兼容部分C字符串函数

虽然SDS都是二进制安全的,但是它一样遵循着C字符串以空字符结尾的惯例,使得SDS可以重用一部分<string.h>库定义的函数,这样SDS就不用自己专门去写一些函数了,避免了不必要的代码重复。

1.3 总结

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

本文为《Redis设计与实现》读书笔记
大佬文章(敖丙):
https://mp.weixin.qq.com/s/uYUQ1P8Dq1Cdknxif7lF-g
https://mp.weixin.qq.com/s/VY31lBOSggOHvVf54GzvYw

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值