乐趣区

关于redis:redis的5种对象与8种数据结构之字符串对象下

简介: 引言 本文是对《redis 设计与实现(第二版)》中数据结构与对象相干内容的整顿与阐明。本篇文章只对对象构造,1 种对象——字符串对象。以及字符串对象所对应的两种编码——raw 和 embstr,进行了具体介绍。

引言

本文是对《redis 设计与实现(第二版)》中数据结构与对象相干内容的整顿与阐明。本篇文章只对对象构造,1 种对象——字符串对象。以及字符串对象所对应的两种编码——raw 和 embstr,进行了具体介绍。表白一些自己的想法与认识,也心愿更多敌人一起来探讨,分享交换。

作者:太阳

云掣科技 - 数据库团队
数据库工程师

字符串对象

字符串对象能够存储整数、浮点数、字符串,具体策略是:

当存储整数时,用到的编码是 int,底层的数据结构能够用来存储 long 类型的整数;
当存储字符串时,如果字符串的长度小于等于 32 字节,那么将用编码为 embstr 的格局来存储;如果字符串的长度大于 32 字节,将用编码为 raw 的 SDS 格局来存储;
当存储浮点数时会先将浮点数转换为字符串,如果转换后的字符串长度小于 32 字节就用编码为 embstr 的格局来存储,否则用编码为 raw 的 SDS 格局来存储。

下图是一个字符串对象的结构图,最左侧是对象构造,两头跟右侧合起来是 raw 编码的 SDS 数据结构(sdshdr),示例图:

raw 编码,简略动静字符串(simple dynamic string-SDS)

redis 用的并不是 C 语言传统的字符串,而是本人构建了简略动静字符串(simpledynamic string,SDS)。

当 redis 打印日志信息或输入报错信息,这些输入的字符串是不会被批改的字符串字面量(sting literal),此时用的是 C 语言传统的字符串来存储这些信息的。当 redis 须要存储的是能够被批改的字符串时,就会应用 SDS 构造。

除了用来保留数据库中的字符串值之外,SDS 还被用作缓冲区(buffer):AOF 模块中的 AOF 缓冲区,以及客户端状态中的输出缓冲区,都是由 SDS 实现的。

SDS 的构造

SDS 构造示意图如下所示:

sdshdr 是该数据结构的名称即 SDS,其中:

buf 属性,是一个字节数组,用来保留字符串,前面箭头对应的就是理论保留的字符串内容,最初以’0’空字符串结尾;

len 属性,记录的是 buf 数组中理论已应用的字节数量,等于 SDS 所保留字符串的长度;

free 属性,记录的是 buf 数组中未应用字节的数量。

SDS 长处

一、能够用 O(1)的复杂度获取到字符串长度

SDS 的 len 属性记录了字符串的长度,而传统 C 字符串要想晓得长度须要遍历整个字符串。相比于传统 C 字符串,redis 获取字符串长度所需的复杂度从 O(N)升高到了 O(1)。

即便对十分长的字符串重复执行 STRLEN 命令(获取字符串长度),也不会造成过多的性能耗费。

二、杜绝缓冲区溢出

在传统的 C 字符串中,如果要批改字符串的内容,但批改后字符串的长度超过原先的长度就会产生溢出景象。详见下图:

在 SDS 中,当须要对 buf 字节数组中存储的内容进行批改 (削减或删除) 时,API 会先通过 free 和 len 属性查看 SDS 的空间是否足够,如果不够的话,SDS 会主动扩大空间再对内容进行批改。对于主动扩大空间的策略见下方“空间预调配”的内容。

三、缩小批改字符串长度时所需的内存重调配次数

对于传统 C 字符串:

如果执行的是增长字符串的操作,如拼接操作(append),那么在执行命令之前,程序须要先通过内存重调配来扩大底层数据的空间大小——否则会产生缓冲区溢出。

如果执行的是缩短字符串的操作,如截断操作(trim),那么在执行这个操作之后,程序须要通过内存重调配来开释字符串不再应用的空间——否则会产生内存透露。

对于 redis 中的 SDS 构造:

内存重调配设计简单的算法,是一个比拟耗时的操作,redis 作为速度要求严苛、数据会被频繁执行的数据库,如果每次批改字符串都须要进行一次内存重调配,会重大影响性能。

应用 SDS,buf 数组里能够蕴含未应用的字节,这些字节的数量由 free 属性记录,能够缩小批改字符串长度时所需的内存重调配次数。

空间预调配和惰性空间开释

通过 SDS 中 free 属性定义的未应用空间,SDS 能够实现空间预调配和惰性空间开释两种优化策略:

1、空间预调配策略——能够升高字符串增长操作引起的内存重调配

当须要批改 SDS 的内容,且须要进行空间扩大的时候,程序不仅会为 SDS 调配批改所需的必须空间,还会为 SDS 调配额定的未应用空间。

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

如果对 SDS 进行批改之后,SDS 的长度 (即 len 属性的值) 将小于 1MB,那么程序将调配和 len 属性同样大小的未应用空间,这时 SDS len 属性的值将和 free 属性的值雷同。

如果对 SDS 进行批改后,SDS 的长度将大于等于 1MB,那么程序会调配 1MB 的未应用空间。

阐明

如果对一个字符串的开端继续追加内容,当字符串整体大小大于 1MB 时,即便只追加一字节的字符,程序也会额定调配 1MB 的空间,当再次追加一字节的字符时,程序不会再额定调配 1MB 的空间,而是应用已有的闲暇空间。

即在扩大空间之前,会先查看未应用的空间是否足够,如果足够,是不会额定再扩大的。

通过空间预调配策略,SDS 将间断增长 N 次字符串所需的内存重调配次数从必然 N 次升高为最多 N 次。

2、惰性空间开释策略——能够升高字符串缩短操作引起的内存重调配

当 SDS 中的字符串长度被缩短时,程序并不会立刻应用内存重调配来回收缩短后多进去的字节空间,而是应用 free 属性将这些字节的数量记录起来,以备未来应用。

当然,redis 提供了相应的命令来真正开释这些未应用空间,防止不必要的内存节约。

四、二进制平安

C 字符串中的字符必须合乎某种编码(比方 ASCII),并且除了字符串的开端之外,字符串外面不能蕴含空字符,如果字符串除开端外还有其它空字符,那么最先被程序读入的空字符将被误认为是字符串结尾,这些限度使得 C 字符串只能保留文本数据,而不能保留图片、音频、视频、压缩文件这样的二进制数据。

为了确保 redis 能够实用于各种不同的应用场景,SDS 的 API 都是二进制平安的(binary-safe),所有 SDS API 都会以解决二进制的形式来解决 SDS 寄存 buf 数组里的数据,程序不会对其中的数据做任何限度、过滤或者假如,数据在写入时是什么样的,它被读取时就是什么样。

这也是 RDS 的 buf 属性被称为字节数组的起因——redis 不是用这个数组来保留字符,而是用它来保留一系列二进制数据。

五、兼容局部 C 字符串函数

SDS 遵循空字符串结尾这一常规,益处是能够间接重用 C 字符串函数库里的函数,从而防止了不必要的代码反复。

embstr 编码

如果字符串对象保留的是长度小于等于 32 字节的字符串,那么将会应用 embstr 编码,embstr 编码是专门用来保留短字符串的一种优化编码方式。embstr 编码与 raw 编码对应的字符串对象,都是由对象构造 (redisObject) 和数据结构 (sdshdr) 组成的。

区别在于用 raw 编码的字符串对象会调用两次内存调配函数来别离创立 redisObject 构造和 sdshdr 构造,而 embstr 编码则通过调用一次内存调配函数来调配一块间断的空间,空间中一次蕴含 redisObject 和 sdshr 两个构造,embstr 编码的字符串对象结构图如下所示:

两者的区别

embstr 编码的字符串对象在执行命令时,产生的成果和 raw 编码的字符串对象执行命令时产生的成果是雷同的的,但应用 embstr 编码的字符串对象来保留短字符串值有以下益处:

1、embstr 编码将创立字符串对象所需的内存调配次数从 raw 编码的两次升高为一次;

2、开释 embstr 编码的字符串对象只须要调用一次内存开释函数,而开释 raw 编码的字符串对象须要调用两次内存开释函数;

3、embstr 编码的字符串对象的所有数据都保留在一块间断的内存里,构造更加紧凑,而 raw 编码是扩散开的,redisObject 对象构造和 sdshdr 数据结构彼此间是用指针相关联的,embstr 编码的对象比 raw 编码的对象可能更好的利用缓存带来的劣势。

编码的转换

int 编码的字符串对象和 embstr 编码的字符串对象在条件满足的状况下,会被转换成 raw 编码的字符串对象。encoding 命令能够查看键对应的值,底层用的是什么编码。

int 转换为 raw

对于 int 编码的字符串对象来说,如果咱们向对象执行了一些命令,使得这个对象保留的不再是整数值,而是一个字符串值,那么字符串对象的编码将从 int 变为 raw。

27.0.0.1:6379> set a 100    // 设置 a =100
OK
127.0.0.1:6379> object encoding a    // 查看键 a 存储的值用的是什么编码
"int"    
127.0.0.1:6379> append a 'a'  // 向键 a 的值中追加内容’a’,此时键 a 存储的值将变为字符串类型
(integer) 4
127.0.0.1:6379> get a  // 查询键 a 的值
"100a"  
127.0.0.1:6379> object encoding a    // 查看键 a 存储的值当初对应的编码,发现曾经变为 raw 格局的编码,示意外面当初存储的是字符串
"raw"

int 编码的字符串,存储的是 long 类型的整数,范畴是 2^63-1(2 的 63 次方减一) ~ -2^63(2 的 63 次方),当存储的整数在该范畴内时,编码为 int,当值超过该范畴,编码将转换为 embstr。

27.0.0.1:6379> set number1 9223372036854775807  
OK
127.0.0.1:6379> object encoding number1    
"int"
127.0.0.1:6379> set number2 9223372036854775808  
OK
127.0.0.1:6379> object encoding number2
"embstr"
127.0.0.1:6379> set number3 -9223372036854775808
OK
127.0.0.1:6379> object encoding number3
"int"
127.0.0.1:6379> set number4 -9223372036854775809
OK
127.0.0.1:6379> object encoding number4
"embstr"

embstr 转换为 raw

embstr 编码的字符串对象无奈被批改(redis 没有为 embstr 编码的字符串对象编写任何响应的批改程序),只有 int、raw 编码的字符串对象能够被批改,所以 embstr 编码的字符串实际上是只读的。

当对 embstr 编码的字符串对象执行任何批改命令时,程序都会先将对象的编码从 embstr 转换为 raw,而后再执行批改命令。所以一旦 embstr 编码的字符串被批改,它的数据结构就会变成 raw 编码的格局。

127.0.0.1:6379> set a 'ab'
OK
127.0.0.1:6379> object encoding a
"embstr"
127.0.0.1:6379> append a 'c'
(integer) 
3127.0.0.1:6379> get a
"abc"
127.0.0.1:6379> object encoding a
"raw"

以上就是依据《redis 设计与实现(第二版)》中数据结构与对象相干内容进行的局部整顿与分享,欢送各位独特参加探讨一起交换沟通。

云掣科技,一家专一于云托管(MSP)服务的企业

退出移动版