0
点赞
收藏
分享

微信扫一扫

Redis 字符串用起来简单,但是原理可是真不简单


无论你现在使用什么编程语言,每天最高频使用的应该就是字符串。可以说字符串对象很基础,也很重要。

那么今天想跟大家聊聊 Redis 字符串相关实现,来看下这个看起来简单的字符串,为什么实现起来确实不简单?

看完这篇文章你可以学到:

  • Redis 字符串对象多种数据结构
  • 底层数据结构转换关系
  • SDS 动态字符串
  • Redis 采用 C 语言实现,那么字符串为什么不直接使用 C 语言的字符串?

Redis 对象

每当我们在 Redis 中保存一对新的键值对时,Redis 至少会创建两个对象,一个对象用作保存键,而另一个对象用作保存值。

Redis 中每个对象都是一个 redisObject 结构,这个结构中有三个属性:

  • type,表示对象的类型
  • encoding,表示编码
  • pstr,指向底层数据结构的指针



Redis 字符串用起来简单,但是原理可是真不简单_python


type 代表对象类型,目前可以使用类型为:

  • 字符串对象
  • 列表对象
  • 哈希对象
  • 集合对象
  • 有序集合对象



Redis 字符串用起来简单,但是原理可是真不简单_字符串_02


我们可以使用 Redis 的 ​​TYPE​​ 指令,查看当前键对应值对象类型。



Redis 字符串用起来简单,但是原理可是真不简单_java_03


Redis 键对象说起来很简单,它总是是一个字符串对象,而值对象就相对复杂了,它可以是字符串,也可以是集合,也可以是字典等对象。

每个对象类型底层实现的时候将会采用了多种数据结构,而 encoding 代表底层数据结构类型:



Redis 字符串用起来简单,但是原理可是真不简单_python_04


我们可以使用 Redis 的 ​​OBJECT ENCODING​​ 指令查看一个当前键值对象底层的编码:



Redis 字符串用起来简单,但是原理可是真不简单_java_05


每个对象类型,可能支持多种数据结构,关系如下:



Redis 字符串用起来简单,但是原理可是真不简单_python_06


Redis 字符串对象

Redis 字符串对象底层支持三种数据结构,分别是(下面使用 OBJECT ENCODING

  • ​int​
  • ​embstr​
  • ​raw​

int 代表 long 类型的整数,只要字符串对象中保存的是一个整数值,就将会使用 ​​int​​ 。



Redis 字符串用起来简单,但是原理可是真不简单_java_07


这里需要注意,只有整数才会使用 ​​int​​,如果是浮点数, Redis 内部其实先将浮点数转化为字符串值,然后再保存。

​embstr​​​ 与 ​​raw​​ 类型底层的数据结构其实都是 SDS (简单动态字符串),Redis 内部定义 sdshdr 一种结构。

那这两者的区别其实在于,​​embstr​​​类型将会调用内存分配函数,分配一块连续的内存空间,空间中依次包含 ​​redisObject​​​ 与 ​​sdshdr​​ 两个数据结构。

而 ​​raw​​​ 类型将会调用两次内存分配函数,分配两块内存空间,一块用于包含 ​​redisObject​​​结构,而另一块用于包含 ​​sdshdr​​ 结构。



Redis 字符串用起来简单,但是原理可是真不简单_redis_08


SDS 简单动态字符串

接下来我们来看下简单动态字符串。

sdshdr

sds 底层的结构包含三个属性:

  • ​len​​​,用于记录下面​​buf[]​​ 数组已使用的长度,即等于 SDS 所保存的字符串长度
  • ​free​​​,用于记录下面​​buf[]​​ 数组未使用长度
  • ​buf[]​​,用户保存字符串



Redis 字符串用起来简单,但是原理可是真不简单_python_09


这里我们需要注意了,​​buf[]​​​数组中最后一个字节将会保存一个空字符 ​​\0​​​,代表字符串结束。sdshdr 结构中的 ​​len​​ 长度是没有包含这个这一个空字符。

这一点设计上与 C 语言的字符串相同。

SDS 扩容

当我们对 SDS 字符串进行增长操作,如果 SDS 空间不足,SDS 将会先进行扩容,然后再执行修改操作。

假设当前 SDS 值如下所示:



Redis 字符串用起来简单,但是原理可是真不简单_redis_10


现在执行一次字符串拼接指令,


APPEND sds " Cluster"


由于拼接完成之后字符串总长度为 13,SDS 剩余空间不足,所以 SDS 将进行扩容,重新执行一次内存重分配。

由于我们上面的例子,SDS 修改之后的长度(即 len 属性值)小于 1MB,那么程序将会分配和 ​​len​​ 属性同样大小的未使用空间,此时 SDS 中 len 属性与 free 属性值相同。

此时 SDS ​​buf​​ 数组的实际长度为:


13+13+1=27




Redis 字符串用起来简单,但是原理可是真不简单_python_11


如果 SDS 修改之后长度大于 1MB,那么 Redis 将会分配 1MB的未使用空间。

假设进行修改之后,SDS ​​len​​ 属性变为 20MB,那么程序将会分配 1 MB 的未使用空间,此时 SDS ​​buf​​ 数组的实际长度为:


20MB+1MB+1byte


接着上面的例子,如果我们再次执行字符串拼接指令:


APPEND sds " Guide"


这次 SDS 未使用空间足够保存拼接字符串,所以这次不需要重新分配内存。



Redis 字符串用起来简单,但是原理可是真不简单_java_12


SDS 惰性空间释放

当我们对字符串进行缩减操作, SDS 字符串值将会被缩短,这样 SDS 剩余空间将会变多。

此时程序不会立即就使用内存分配函数回收多余空间,而是使用 ​​free​​ 属性记录多余空间,等待后面使用。

通过这种惰性空间释放策略,SDS 避免字符串缩短所需内存重分配操作,后续 SDS 字符串增长,可以直接使用多余的空间。

当然 SDS 也有相应的 API,可以真正释放 SDS 未使用的空间,所以不用担心惰性释放策略带来的内存浪费。

为什么 Redis 不直接使用 C 语言字符串?

Redis 底层使用 C 语言编程,那么其实 C 语言也有字符串,Redis 完全可以复用 C 语言字符串~

那么为什么 Redis 还要闭门造车,重新设计一个 SDS 数据结构呢?

这是因为 C 原因这种简单的字符串形式,在安全性,效率以及功能方面不满足 Redis 需求。

首先如果我们需要获取 C 语言字符串的长度,我们可以使用以下函数:


strlen(str)


​strlen​​函数实际上使用遍历的方式,对每个字符计数,直到遇到代表字符串结束的空字符。

这个操作时间复杂度 O(N)

而 SDS 由于 ​​len​​ 记录当前字符串的长度,所以直接读取即可,时间复杂度仅为 O(1)

这就确保获取字符串长度的操作不会成为 Redis 性能瓶颈。

第二点每次增长或缩短 C 语言的字符串将会进行内存的重分配,否则可能导致缓冲区溢出,也有可能导致内存泄漏。

而 SDS 由于使用空间预分配的策略,如果 SDS 连续增长 N 次,内存重分配的次数从必定 N 次降低为最多 N 次。

第三点,C 语言字符串不能包含空字符,否则第一个空字符将会被误认为字符串结尾,这就大大限制使用的场景。

而 SDS 中由于可以使用 ​​len​​ 属性的值判断字符串是否结束,所以没有这种困扰。

所以 SDS 字符串是二进制安全的。

字符串对象底层数据结构转换

SDS 结构由于需要额外的属性记录长度以及未使用长度,虽然这样减少系统的复杂度,提高了性能,但是还是付出相应的代价,即存在一定内存空间浪费。

所以 Redis 字符串对象底层结构并不都是采用了 SDS。

如果字符串对象保存的是整数值,那么这个整数值将会直接保存在字符串对象结构的 ​​ptr​​ 属性。



Redis 字符串用起来简单,但是原理可是真不简单_python_13


如果保存不是整数,但是字符串长度小于等于 39 字节,那么字符串将会使用 ​​embstr​​​编码方式。最后上述两个都不满足才会使用 ​​raw​​ 编码方式。

另外,​​int​​​ 编码与 ​​embstr​​​,在一定条件也将会转换为 ​​raw​​ 编码格式。

如果对底层整数执行追加操作,使其不再是一个整数,则字符串对象的编码就会从 ​​int​​​ 变为 ​​raw​​。

对于 ​​embstr​​​ 编码,实际其实只读的,这是因为 Redis 底层并没有提供任何程序修改 ​​embstr​​ 编码对象。

所以一旦我们对​​embstr​​​ 编码字符串进行修改,字符串对象的编码就会从 ​​embstr​​​ 变为 ​​raw​​。

总结

Redis 字符串是我们平常操作最频繁的数据结构,了解底层字符串对象,对于我们了解 Redis 非常重要的。

Redis 字符串对象底层结构可以分为整数与 SDS,当我们在操作 ​​set​​ 命令保存整数时,就将会直接使用整数。

而 SDS 是 Redis 内部定义另一种结构,相比于 C 语言字符串,它可以快速计算字符串长度,不需要频繁的分配的内存,最终要一点 SDS 是一种二进制安全的字符串。

SDS 设计虽然带来的很多优势,但是这种结构相比于 C 语言,至少多占用 4(len)+4(free)=8 字节内存,如果 ​​buf []​​数组还存在没有使用的空间,空间浪费更加严重。

所以 Redis 字符串不是万金油,某些场景下可能会占用大量内存。但是如果换一种数据结构,可能内存占用就会变少。

所以我们一定要基于合适场景使用合适的数据机构。

原作者:鸭血粉丝



Redis 字符串用起来简单,但是原理可是真不简单_python_14


举报

相关推荐

0 条评论