《Redis专题》-String数据类型之底层解析

1.引言

对于 Redis的常用 5 种数据类型(StringHashListSetsorted set),每种数据类型都提供了 最少两种内部的编码格式,而且每个数据类型内部编码方式的选择对用户是完全透明的,Redis会根据数据量自适应地选择较优化的内部编码格式。
在这里插入图片描述
如果想查看某个键的内部编码格式,可以使用 OBJECT ENCODING keyname指令来进行,比如:

127.0.0.1:6379> 
127.0.0.1:6379> set foo bar
OK
127.0.0.1:6379> 
127.0.0.1:6379> object encoding foo  // 查看某个Redis键值的编码
"embstr"
127.0.0.1:6379> 
127.0.0.1:6379>

2.疑问与解析

结构图上显示,String类型有三种实现方式:

  • 使用整数值实现的字符串对象
  • 使用embstr编码的动态字符串实现的字符串对象
  • 动态字符串实现的字符串对象

疑问embstr是什么意思,动态字符串又是什么意思?字符串对象到底什么结构?三种实现方式有什么区别呢?
在这里插入图片描述
不急,咱们一步一步的往下看:

2.1.Redis中定义的对象的结构体

Redis使用前面说的五大数据类型来表示键和值,每次在Redis数据库中创建一个键值对时,至少会创建两个对象,一个是键对象,一个是值对象,而Redis中的每个对象都是由 redisObject 结构来表示:

typedef struct redisObject{
     //类型 4bits
     unsigned type:4;
     //编码 4bits
     unsigned encoding:4;
     //指向底层数据结构的指针 64-bit
     void *ptr;
     //引用计数  32bits
     int refcount;
     //记录最后一次被程序访问的时间  24bits
     unsigned lru:22;

}robj

注释:type表示该对象的类型,即上面 [String,List,Hash,Set,Zset] 中的一个,但为了提高存储效率与程序执行效率,每种对象的底层数据结构实现都可能不止一种,encoding 表示对象底层所使用的编码。

① type属性

对象的type属性记录了对象的类型,这个类型就是前面讲的五大数据类型:

在这里插入图片描述
可以通过如下命令来判断对象类型:

type key

在这里插入图片描述

注意:在Redis中,键总是一个字符串对象,而值可以是字符串、列表、集合等对象,所以我们通常说的键为字符串键,表示的是这个键对应的值为字符串对象,我们说一个键为集合键时,表示的是这个键对应的值为集合对象。

②、encoding 属性和 *prt 指针
对象的 prt 指针指向对象底层的数据结构,而数据结构由 encoding 属性来决定。

在这里插入图片描述
而每种类型的对象都至少使用了两种不同的编码:
在这里插入图片描述
可以通过如下命令查看值对象的编码:

OBJECT ENCODING  key 

比如 string 类型:(可以是 embstr编码的简单字符串或者是 int 整数值实现)
在这里插入图片描述

2.2.embstr与动态字符串

embstr :是专门用于保存短字符串的一种优化编码方式,跟正常的字符编码相比,字符编码会调用两次内存分配函数来分别创建 redisObjectsdshdr 结构(动态字符串结构),而 embstr 编码则通过调用一次内存分配函数来分配一块连续的内存空间,空间中包含 redisObjectsdshdr(动态字符串)两个结构,两者在同一个内存块中。从 Redis 3.0 版本开始,字符串引入了 embstr 编码方式,长度小于 OBJ_ENCODING_EMBSTR_SIZE_LIMIT(39) 的字符串将以EMBSTR方式存储。

注意: 在Redis 3.2 之后,就不是以 39 为分界线,而是以 44 为分界线,主要与 Redis 中内存分配使用的是 jemalloc 有关。( jemalloc 分配内存的时候是按照 8、16、32、64 作为 chunk 的单位进行分配的。为了保证采用这种编码方式的字符串能被 jemalloc 分配在同一个 chunk 中,该字符串长度不能超过64,故字符串长度限制

OBJ_ENCODING_EMBSTR_SIZE_LIMIT = 64 - sizeof(‘0’)为1 - sizeof(robj) 为16 - sizeof(struct sdshdr)为8 = 39)

动态字符串 :Redis 自己构建的一种名为 简单动态字符串(simple dynamic string,SDS)的抽象类型,并将 SDS 作为 Redis 的默认字符串表示。先简单了解概念,后面看详细解析

2.3.字符串对象转换

字符串对象的编码可以是int,raw或者embstr。
1、int 编码:保存的是可以用 long 类型表示的整数值。
2、raw 编码:保存长度大于44字节的字符串(redis3.2版本之前是39字节,之后是44字节)。
3、embstr 编码:保存长度小于44字节的字符串(redis3.2版本之前是39字节,之后是44字节)。

也就是说,Redis 会根据当前值的类型和长度决定使用内部编码实现

2.4.实践验证

命令:object encoding key ,获取数据底层的数据结构

1)整数类型示例如下:
在这里插入图片描述
2)短字符串示例如下:
在这里插入图片描述
3)长字符串示例如下:
在这里插入图片描述

3.字符串对象转换

众所周知,Redis 是用 C 语言写的,但是对于 Redis 的字符串,却不是 C 语言中的字符串(即以空字符 ’\0’ 结尾的字符数组),它是自己构建了一种名为 简单动态字符串(simple dynamic string,SDS)的抽象类型,并将 SDS 作为 Redis 的默认字符串表示。

3.1.动态字符串结构分析

struct sdshdr{
     //记录buf数组中已使用字节的数量
     //等于 SDS 保存字符串的长度 4byte
     int len;
     //记录 buf 数组中未使用字节的数量 4byte
     int free;
     //字节数组,用于保存字符串 字节\0结尾的字符串占用了1byte
     char buf[];
}

用SDS保存字符串 “Redis” 具体结构如下图
在这里插入图片描述
对于 SDS 数据类型的定义:

len 保存了SDS保存字符串的长度
buf[] 数组用来保存字符串的每个元素
free 记录了 buf 数组中未使用的字节数量

上面的定义相对于 C 语言对于字符串的定义,多出了 len 属性以及 free属性。为什么不直接使用 C 语言字符串实现,而是要使用 SDS 呢?有什么特别的优势呢?

3.2.SDS结构与C语言字符串结构比较分析

在这里插入图片描述
1)获取字符串长度复杂度
  sdshdr 中由于 len 属性的存在,获取 SDS 字符串的长度只需要读取 len 属性,时间复杂度为 O(1),而对于 C 语言来说,获取字符串的长度通常是遍历字符串计数来实现的,时间复杂度为 O(n)。

2)API安全性与缓冲区溢出
缓冲区溢出(buffer overflow):是这样的一种异常,当程序将数据写入缓冲区时,会超过缓冲区的边界,并覆盖相邻的内存位置。在 C 语言中使用 strcat 函数来进行两个字符串的拼接,一旦没有分配足够长度的内存空间,就会造成缓冲区溢出,如
在这里插入图片描述
s1 = ‘Redis’s2 = ‘MongoDB’,当执行strcat(s1, " Cluster")时,未给 s1 分配足够内存空间,s1 的数据将溢出到 s2 所在的内存空间,导致 s2 保存的内容被意外地修改。
在这里插入图片描述
由于 SDS 记录了自身长度,同时在修改时,API 会按照如下步骤进行:

(1)先检查SDS的空间是否满足修改所需的要求;
(2)如果不满足要求的话,API 会自动将 SDS 的空间扩展至执行修改所需的大小(realloc);
(3)然后才执行实际的修改操作;所以SDS不会造成缓冲区溢出情况

3)字符串的内存重分配次数
 C 语言由于不记录字符串的长度,所以如果要修改字符串,必须要重新分配内存。
 SDS 实现了空间预分配和惰性释放两种策略:

(1)空间预分配:当 SDS 的 API 对一个 SDS 进行修改,并且需要对 SDS 进行空间扩展的时候,程序不仅会为 SDS 分配修改所必须的空间,还会为 SDS 分配额外的未使用空间,这样可以减少连续执行字符串增长操作所需的内存重分配次数。

(2)惰性释放:当 SDS 的 API 需要对 SDS 保存的字符串进行缩短时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用 free 属性将这些字节的数量记录起来,并等待将来使用,如
在这里插入图片描述

sdstrim(s, "XY"); // 移除 SDS 字符串中的所有 'X''Y' 

结果
在这里插入图片描述
4)二进制数据安全
二进制安全(binary-safe):指能处理任意的二进制数据,包括非 ASCII 和 null 字节。

C 字符串以空字符 ‘\0’,作为字符串结束的标识,而对于一些二进制文件(如图片等),内容可能包括空字符串’\0’,导致程序读入的空字符会被误认为是字符串的结尾,因此C字符串无法正确存取二进制数据;

SDS 的 API 都是以处理二进制的方式来处理 buf 里面的元素,并且 SDS 不是以空字符串’\0’来判断是否结束,而是以 len 属性表示的长度来判断字符串是否结束,

因此 Redis 不仅可以保存文本数据,还可以保存任意格式的二进制数据。

5)C字符串函数兼容
SDS 的buf数组会以’\0’结尾,这样可以重用 C 语言库<string.h> 中的一部分函数,避免了不必要的代码重复。

4.字符串对象

字符串对象的编码可以是int、raw或者embstr

4.1.int编码

如果一个字符串对象保存的是整数值,并且这个整数值可以用long类型来表示,那么字符串对象会将整数值保存在字符串对象结构的ptr属性里面,并将字符串对象的编码设置为int

127.0.0.1:6379> SET number 10086
OK
127.0.0.1:6379> OBJECT ENCODING number
"int"

在这里插入图片描述

4.2.raw编码

如果字符串对象保存的是一个字符串值,并且这个字符串值的长度大于39字节,那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串值,并将对象的编码设置为raw

127.0.0.1:6379> SET story "Long , long, long ago there lived a king ..."
OK
127.0.0.1:6379> STRLEN story
(integer) 44
127.0.0.1:6379> OBJECT ENCODING story
"embstr"

在这里插入图片描述

4.3.embstr编码

如果字符串对象保存的是一个字符串值,并且这个字符串的长度小于等于39字节,那么字符串对象将使用embstr编码的方式来保存这个字符串值

embstr编码raw编码都使用redisObject结构和sdshdr结构来表示字符串对象,但raw编码会调用两次内存分配函数来分别创建redisObject结构和sdshdr结构,而embstr编码则通过调用一次内存分配函数来分配一块连续的空间,空间中依次包含redisObject结构和sdshdr两个结构

embstr编码产生的内存块结构:
在这里插入图片描述
embstr编码的字符串对象来保存短字符串值有以下好处:

  • embstr编码将创建字符串对象所需的内存分配次数从raw编码的两次降低为一次
  • 释放embstr编码的字符串对象只需要调用一次内存释放函数,而释放raw编码的字符串对象需要调用两次内存释放函数
  • 因为embstr编码的字符串对象的所有数据都保存在一块连续的内存里面,所以这种编码的字符串对象比起raw编码的字符串对象能够更好地利用缓存带来的优势
127.0.0.1:6379> SET msg "hello"
OK
127.0.0.1:6379> OBJECT ENCODING msg
"embstr"

在这里插入图片描述
可以用long double类型表示的浮点数在Redis中也是作为字符串值来保存的

5.编码的转换

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

127.0.0.1:6379> SET number 10086
OK
127.0.0.1:6379> OBJECT ENCODING number
"int"
127.0.0.1:6379> APPEND number " is a good number!"
(integer) 23
127.0.0.1:6379> GET number
"10086 is a good number!"
127.0.0.1:6379> OBJECT ENCODING number
"raw"

因为Redis没有为embstr编码的字符串对象编写任何相应的修改程序,所以embstr编码的字符串对象实际上是只读的。当我们对embstr编码的字符串对象执行任何修改命令时,程序会先将对象的编码从embstr转换成raw,然后再执行修改命令。因为这个原因,embstr编码的字符串对象在执行修改命令之后,总会变成一个raw编码的字符串对象

127.0.0.1:6379> SET msg "hello world"
OK
127.0.0.1:6379> OBJECT ENCODING msg
"embstr"
127.0.0.1:6379> APPEND msg " again!"
(integer) 18
127.0.0.1:6379> OBJECT ENCODING msg
"raw"

6.要点总结

  • String 类型对象三种实现方式,intembstrraw
  • 字符串内容可转为 long,采用 int 类型,否则长度<39(3.2版本前39,3.2版本后分界线44) 用 embstr,其他用 raw
  • SDS 是Redis自己构建的一种简单动态字符串的抽象类型,并将 SDS 作为Redis 的默认字符串表示
  • SDS 与 C 语言字符串结构相比,具有五大优势
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

熊猫-IT

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值