【Redis源码系列】Redis6.0数据结构详解--SDS篇

968 阅读6分钟

前言

上篇文章我们研究了Redis6的DB实现以及数据存储过程, 通过过往的详细分析我们已经了解了Redis6的服务启动, 事件机制, 多线程处理, DB结构及数据存储, 已经掌握了Redis的事件执行流程。伴随深入的学习和了解, 我们越来越接近Redis的核心数据实现, 同时又有了更多的疑问:

  • Redis不同的数据类型底层输入和实现的?
  • Redis对于数据的处理在性能和存储优化方面有什么优秀的方法? 带着这些疑问, 我们开始深入学习Redis的数据结构实现, 本期先学习数据结构--SDS。

前期回顾: Redis6.0 DB结构以及渐进式rehash超详细源码解读

何为SDS?

SDS 全称为 Simple Dynamic String,即简单动态字符串, 在Redis中几乎任何模块都有使用SDS数据结构,那么Redis为何不使用 char 类型作为字符串的实现呢? 通过自定义的结构实现有何优势呢?我们一起来探究。

探究SDS

SDS定义

Redis的SDS定义在文件src/sds.h中, 通过对于不同版本的Redis源码实现学习, 我发现Redis6.0.0Redis3.0.0版本的SDS实现是有比较大的差距的, 3.0版本的定义如下:

image.png

之所以将Redis的字符串实现与PHP7实现的sting结构放在一起来比较, 是因为两者在实现的核心思路上都是极度相似的, 通过在结构体中定义一个len字段, 就可以很方便的解决C语言中字符串读取的效率问题, 我们都是到C语言中的字符串存储通过字符数组来实现, 我们想要读取字符串时就要遍历char[], 直到遇见'\0'为止读取结束, 这也是一种业界比较通用的优秀设计。这带来两个优势:

  • 可以用O(1)的时间复杂度读字符串,极大提升了读取效率;
  • 字符串二进制安全;
  • 有自动扩容, 惰性释放空间机制, 减少内存分配开销;

那么Redis6.0有何种措施呢?6.0版本定义如下:

image.png

字段解释如下:

  • len: 字符数组长度, 不包括 '\0'
  • alloc: 内存分配总长度
  • flags: 标识当前结构类型, 使用底三位存储
  • buf[]: 柔性数组, 用于存储实际字符串数据 flags字段类型如下:

image.png

图中省去了sdshdr5的定义, 因为其未被使用过。基于此定义有两点非常值得我们探究, 第一点就是针对于不同子长的buf使用结构定义, 可以节省header部分长度; 第二点就是结构定义使用了__attribute__ ((__packed__))声明为非内存对齐, 紧凑排列形式。第一点理解比较简单, 但也可见作者对于节省内存的机制追求, 我们重点理解一下为何SDS不使用内存对齐, 而采用紧凑排列的形式, 为此我们首先需要了解一下何为内存对齐。

内存存放数据是为了给CPU进行使用,而CPU访问内存数据时会受到地址总线宽度的限制,并且CPU从内存中获取数据时起始地址必须是地址总线宽度的倍数,也就是说CPU是将内存分为一块一块的,块的大小取决于地址总线宽度,并且CPU读取数据也是一块一块读取的,块的大小称为(memory granularity)内存读取粒度。 举一个简单的例子, 假如CPU地址总线是64位(bit,8字节),有一个int型数据占用4字节内存, 在内存中起始位置为0x06, 那么为了读取这个数据需要CPU执行两次内存访问:

  1. 读取 0x00 - 0x07 字节, 然后保存 0x06 - 0x07 字节到目标前两个字节;
  2. 读取 0x08 - 0xF 字节, 然后保存 0x08 - 0x09 字节到目标后两个字节;

image.png

这样大大降低了CPU性能, 内存对齐就是按照一定规则(CPU读取内存规则)来进行内存排列, 而非顺序存放,相当于使用空间换取时间, 牺牲内存空间, 换取CPU性能, 关于具体的内存对齐规则我们再次不做详细探讨。

SDS使用紧凑模式进行存放, 进一步达到节省内存的目的, 比如对于sizeof(sdshdr16)只会有5个字节占用, 柔型数组不占用内存空间。 除此之外, 之所以定义SDS为 char* 类型,可以兼容C语言的string库函数, 同时如果定义了一个 sds *s, 可以非常方便的使用s[-1]获取到flags地址,避免了在上层调用各种类型判断, 只在SDS内部做类型判断, 对外暴露的结果可认为只有: len, buf等, 是一个非常精妙的设计!

SDS常用API定义

SDS常用的API定义在src/sds.h中, 相对比较简单, 大部分逻辑为获取sds的flags字段然后返回响应的值, 感兴趣大家可以自己查看, 我们做api的相关注释, 简单API如下:

  • sdslen: 获取sds长度
  • sdsavail: 当前sds是否有效, 使用已分配的内存减去长度, 即: alloc - len
  • sdssetlen: 设置sds长度
  • sdsalloc: 获取sds总长度

创建SDS对象

对象创建比较简单, 大致步骤如下:

  1. 声明&定义相关变量
  2. 分配内存
  3. 针对不同的SDS类型做字段赋值
  4. 追加结尾: '\0'

image.png

清空SDS对象

调用创建对象方法, 长度传0即可 image.png

追加扩容操作

扩容操作的逻辑相对也比较简单, 类似于拥塞控制算法的控制, 核心为:

如果新字符串长度小于1024, 即1MB,则扩容双倍,否则在原有基础上增加1024字长内存分配

核心逻辑如下:

image.png

总结

通过上面的分析可知, SDS的设计优点有:

  • O(1)时间复杂度高效读取字符串内容
  • 二进制的字符安全, 不用担心\0截断读取
  • 通过空间预分配和惰性内存回收减少内存分配, 提高性能等 重点是需要理解Redis为什么设计紧凑的内存分步的原因。 至此关于Redis SDS结构的定义和增删改相关操作已经分析完毕, 可见坐着对于内存和性能的优化有着非常极致的追求, 同时也给我们平常的开发工作带来很多启发,欢迎大家一起交流互动, 一起学习进步 :)