Redis底层解析字符串类型

Posted 小生凡一

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis底层解析字符串类型相关的知识,希望对你有一定的参考价值。

SDS

Redis 的底层是C语言,但是其实Redis并没有使用C语言传统的字符串表示,而是自己构建了一种名为简单动态字符串(simple dynamic string,SDS)的抽象类型,并将SDS用作Redis的默认字符串表示。

1. 定义

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

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

SDS 示例

  • free 属性的值为0,表示这个SDS没有分配任何未使用空间
  • len 属性的值为7,表示这个SDS保存了一个七字节长的字符串
  • buf 属性是一个char类型的数组,数组的前七个字节分别保存了FANONEA中的这几个字符,最后一个字节则保留了空字符 ‘\\0’ 。

SDS 遵循C字符串以空字符结尾的惯例,保存空字符的1字节空间不计 算再SDS的 len 属性里面并且为空字符分配额外的1字节空间,以及添加空字符串末尾等操作,都是SDS函数自动完成的。

2. SDS 与 C字符串区别

2.1 优化获取字符串长度时间

因为C字符串并不记录自身的长度信息,所以为了获取一个C字符串的长度,程序必须遍历整个字符串,对遇到的每一个字符都进行计数,直到遇到代表字符串结尾的空字符为止。复杂度O(N)

而SDS在len属性中就已经记录了SDS本身的长度,所以获取一个SDS长度的复杂度为O(1)。设置和更新SDS长度的工作是由SDS的api在执行时自动完成的,使用sds无需进行任何手动修改长度的工作

所以通过使用SDS而不是C字符串,Redis将获取字符串的长度所需要的复杂度从O(N)降低了O(1),这确保了长度的工作不会是redis的性能瓶颈,即时是一个非常长的字符串,我们反复执行 STRLEN 命令,那么这个复杂度始终是O(1),不会对系统造成任何的影响。

2.2 杜绝缓冲区的溢出

C字符串不记录自身长度带来的另一个问题就是容易造成缓冲区溢出。
例如:
<string.h>/strcat 函数将 src 字符串中 的内容拼接到 dest 字符串 的末尾。

char *strcat(char *dest,const char *src)

因为C字符串不记录自身长度,所以strcat假定用户在执行这个函数时,已经为 dest 分配了足够多的内存,可以容纳 src 字符串中的所有内容,而一旦这个假定不成立时,就会产生缓冲区溢出

与C字符串不同的是,SDS的空间 分配策略完全杜绝了发生缓冲区溢出的可能性:
当SDS API需要对SDS进行修改时,API会先检查SDS的空间是否满足修改所需的要求,如果不满足的话,API会自动将SDS的空间扩展至执行修改所需要的大小,然后才执行实际的修改操作,所以使用SDS既不需要手动修改SDS的空间大小,也不会出现前面所说的缓冲区溢出的问题。

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

因为C字符串并不记录自身的长度,所以对于一个包含了N个字符的C字符串来说,这个字符串的底层实现总是一个N+1个字符长的数组(额外的一个字符串空间用来保存空字符串)。

所以C字符串的长度底层数组的长度之间存在着这种关联性,所以 每次增长或是缩短一个C字符串 ,程序都总要对 保存这个C的字符串数组进行一次内存重分配操作

  • 如果要执行append拼接操作,那么就在执行这个操作之前,程序需要先通过内存重分配来扩展底层数组的空间大小,如果忘了这一步就会造成缓冲区的溢出。
  • 如果程序是 trim 缩短字符串的操作,那么在执行这个操作之后,程序需要通过内存重分配来释放字符串不再使用那部分空间,如果忘记了这一步就会产生内存泄漏。

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

所以为了避免这种情况,SDS通过未使用空间free解除了字符串长度和底层数组长度之间的关联:

在SDS中,buf数组的长度不一定是字符数量+1,数组里面可以包含未使用的字节,而这些字节的数量就由SDS的free属性记录。

2.3.1 空间预分配

空间预分配用于优化SDS的字符串增长操作:当SDS的API对一个SDS进行修改,并且需要对SDS进行空间扩展的时候,程序不仅会为SDS分配修改所必须要的空间,还会为SDS分配额外未使用的空间

其中,额外分配的未使用空间数量由以下公式决定:

  1. 如果对SDS进行修改之后,SDS的长度(也就是len属性的值)将小于1MB,那么程序会分配和 len属性同样大小的未使用空间,这时候SDS的 len属性将和 free属性的值一致。
  2. 如果对SDS修改之后的长度大于 1MB。那么程序就会分配1MB的未使用空间

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

2.3.2 惰性空间释放

惰性空间释放用于优化 SDS 的字符串缩短操作:当SDS的API需要缩短SDS保存的字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用free属性将这些字节的数量记录起来,并等待将来使用。

当然,于此同时,SDS也提供了对应的API,让我们可以在有需要的时候,真正地释放SDS的未使用空间,所以不用担心惰性空间释放策略会造成内存浪费。

2.4 二进制安全

C字符串中的字符必须符合某种编码(比如ASCⅡ),并且除了字符串的末尾之外,字符串里面不能包含空字符串,否则最先被程序读入的空字符串将被误认为是字符串结尾,这些限制使用C字符串只能保存文本数据,而不能保存像图片、音频、视频、压缩文件这种的二进制数据。

虽然数据库一般用于存储文本数据,但是数据库保存二进制数据的场景并不少见。因此,为了确保Redis可以适用于各种不同的使用场景,SDS的API都是二进制安全的,所以SDS API都会处理二进制的方式来处理buf数组里的数据,程序不会对其中的数据做任何限制,过滤或是假设。数据写入的时候什么样子,读取出来的时候就是什么样子

这也是我们将SDS的buf属性成为字节数组的原因,Redis不是用这个数组来保存字符,而是用他来保存一系列的二进制数据。

通过使用二进制安全的SDS,而不是C字符串,使用Redis不仅可以保存文本数据,还可以保存任意格式的二进制数据。

2.5 兼容部分C字符串函数

虽然SDS的API都是二进制安全的,但是他们一样遵循C字符串以空字符结尾的惯例:

这些API总会将SDS保存的数据的末尾设置为空字符,并且总会在为buf数组分配空间时,多分配一个字节来容纳这个空字符串,这是为了让那些保存文本数据的SDS可以重用一部分<string.h>库定义的函数。

举个例子:
如果我们有下图一个保存文本数据的SDS值得sds

那么我们就可以重用<string.h>/strcasecmp函数,使用它来对比SDS的buf属性保存的字符串和另一个C字符串:

strcasecmp(sds->buf,"sheep");

这样redis就不用自己专门去写一个函数来对比SDS值和C字符串值了。

以上是关于Redis底层解析字符串类型的主要内容,如果未能解决你的问题,请参考以下文章

redis设计与实现之SDS简单动态字符串

Redis的基本数据类型

《闲扯Redis四》List数据类型底层编码转换

利用Redisson实现分布式锁及其底层原理解析

利用Redisson实现分布式锁及其底层原理解析

利用Redisson实现分布式锁及其底层原理解析