作者推荐 |【Redis技术进阶之路】「原理系列开篇」揭秘高效存储模型与数据结构底层实现(SDS)(二)

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 作者推荐 |【Redis技术进阶之路】「原理系列开篇」揭秘高效存储模型与数据结构底层实现(SDS)

作者推荐 |【Redis技术进阶之路】「原理系列开篇」揭秘高效存储模型与数据结构底层实现(SDS)(一)https://developer.aliyun.com/article/1471143


原理剖析

简单动态字符串(SDS)Simple Dynamic String的缩写,它是Redis中用于表示字符串的核心数据结构。在Redis中,几乎所有的键(key)都是通过SDS来实现的,体现了其高效和灵活性。

从源码层面分析,SDS的实现细节主要集中在sds.hsds.c文件中。其中,sds.h定义了SDS的数据结构和相关操作接口,而sds.c则提供了这些接口的具体实现。此外,sdsalloc.h文件也扮演着重要角色,它负责SDS内存分配和释放的管理,确保SDS在动态扩展和收缩时能够高效、安全地使用内存。

通过深入分析sds.h头文件,我们可以发现Redis源码为char*类型定义了一个别名,如下所示:

c

复制代码

#ifndef __SDS_H
#define __SDS_H
#define SDS_MAX_PREALLOC (1024*1024)
extern const char *SDS_NOINIT;
#include <sys/types.h>
#include <stdarg.h>
#include <stdint.h>
typedef char *sds;

这种别名的使用在Redis的源代码中非常普遍,尤其是在处理字符串时。通过将char*重命名为sds,Redis不仅使代码更具可读性,还强调了这些指针实际上是指向动态字符串(SDS)的,而不仅仅是普通的C字符串。

数据结构

SDS是Redis用来表示字符串的核心数据结构,它提供了一种动态调整字符串长度的方式,同时还包含了一些额外的元数据,如字符串的长度和已分配的内存大小,还有一个名为flags的元数据字段,该字段的最低3位用于标识SDS的类型。

在SDS的设计中,为了节省内存并适应不同大小的字符串,Redis定义了五种不同大小的SDS头结构,分别是sdshdr5sdshdr8sdshdr16sdshdr32sdshdr64

这五种类型的主要区别在于它们如何存储和管理字符数组的两个关键元数据:现有长度(len)和分配空间长度(alloc)。这些元数据的数据类型因SDS类型的不同而有所区别。

以下是sds.h中定义的五种sdshdr结构,它们分别代表了五种不同的SDS数据结构模型:

sdshdr8sdshdr16sdshdr32sdshdr64这些头结构的大小分别为5字节、8字节、16字节、32字节和64字节。选择哪种头结构取决于字符串的实际长度和已分配的内存大小。

__attribute__((__packed__))是一个GCC编译器指令,用于确保结构体在内存中的布局是紧凑的,即没有额外的填充字节。这对于嵌入式系统或需要精确控制内存布局的场景非常有用。

分析结构体

接下来,我们将逐一深入剖析这些结构体。在结构模型的设计中,我们主要将其划分为两大类别:sdshdr5与其他类型。那么,为何要进行这样的分类呢?让我们来一探究竟。

sdshdr5(没有使用)

Redis的源码中并没有使用,因为它只包含一个flags字段和一个Buf字段,没有足够的空间来存储字符串的长度信息。

  • Flags字段的低3位用于表示SDS的类型(在这个情况下是5),而高5位被浪费了,没有使用。
  • Buf字段是字符数组,用于存储实际的字符串数据。
sdshdr8sdshdr16sdshdr32sdshdr64

sdshdr8sdshdr16sdshdr32sdshdr64这些结构体都包含LenAllocFlagsBuf字段。

  • Len字段表示当前字符串使用的长度(不包括末尾的空字符'\0')。
  • Alloc字段表示已分配的内存大小(不包括头信息和末尾的空字符'\0')。
  • Flags字段的低3位用于表示SDS的类型(8、16、32或64),而高5位没有被使用。
  • Buf字段是字符数组,用于存储实际的字符串数据。

注意,LenAlloc字段的大小会根据SDS头结构的大小而变化。在sdshdr8中,这两个字段都是uint8_t类型,而在sdshdr64中,这两个字段都是uint64_t类型。这种设计允许Redis根据字符串的实际大小动态地选择最合适的头结构,从而节省内存。

下面是一个底层存储案例,大家应该可以看到对应的存储模型是什么样子的。

特点分析
  • 空字符结尾:SDS遵循C字符串的传统做法,即在字符串末尾添加一个空字符(null terminator),以确保字符串的正确终止。这个空字符所占用的1字节空间并不计算在SDS的len属性中,这意味着对于SDS的使用者来说,这个空字符是完全不可见的。
  • 额外的分配:为了确保空字符的存在,SDS会自动为其分配额外的1字节空间,并在需要时在字符串末尾添加空字符。这些操作都是由SDS函数自动完成的,因此用户无需关心。
  • 兼容C方法:SDS可以直接使用C标准库中的头文件所提供的printf函数来展示其内部保存的字符串内容。录例如,通过执行printf("%s", s->buf);语句,可以方便地打印出SDS结构体中buf指针所指向的字符数组。
空字符结尾优点
  1. SDS能够直接重用C标准库中的一部分字符串函数,从而提高了代码的复用性和可维护性。
  2. 空字符的存在也有助于确保字符串的正确性和安全性,防止了越界访问等潜在问题。

特性对比

特征 C字符串 SDS
获取字符串长度的复杂度 O(n) O(1)
API安全性 不安全,可能会造成缓冲区溢出 安全,不会造成缓冲区溢出
内存重分配 修改字符串长度N次必然需要执行N次内存重分配 修改字符串长度N次最多需要执行N次内存重分配
数据类型 只能保存文本数据 可以保存文本或者二进制数据
使用的库函数 可以使用所有<string.h>库中的函数 可以使用一部分<string.h>库中的函数

数据长度分析

sdslen 函数通过解析 SDS 字符串的标志位和头结构来获取字符串的长度,并根据字符串的类型选择相应的计算方式。这种设计允许 Redis 使用不同大小的头结构来存储不同长度的字符串,从而节省内存空间。

c

复制代码

#define SDS_TYPE_5  0
#define SDS_TYPE_8  1
#define SDS_TYPE_16 2
#define SDS_TYPE_32 3
#define SDS_TYPE_64 4
#define SDS_TYPE_MASK 7
#define SDS_TYPE_BITS 3
#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));
#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))
#define SDS_TYPE_5_LEN(f) ((f)>>SDS_TYPE_BITS)
static inline size_t sdslen(const sds s) {
    unsigned char flags = s[-1];
    switch(flags&SDS_TYPE_MASK) {
        case SDS_TYPE_5:
            return SDS_TYPE_5_LEN(flags);
        case SDS_TYPE_8:
            return SDS_HDR(8,s)->len;
        case SDS_TYPE_16:
            return SDS_HDR(16,s)->len;
        case SDS_TYPE_32:
            return SDS_HDR(32,s)->len;
        case SDS_TYPE_64:
            return SDS_HDR(64,s)->len;
    }
    return 0;
}

这段代码是 Redis 中用于获取 SDS(Simple Dynamic String,简单动态字符串)长度的函数 sdslen 的实现。SDS 是 Redis 用于表示字符串的内部数据结构,它包含了一个长度字段和一个预分配的空间字段,以便在需要时能够快速地扩展或收缩字符串。

分析代码

  1. 方法参数以及返回值
  • size_t:这是函数的返回类型,表示字符串的长度。
  • const sds s:这是函数的参数,s 是一个指向 SDS 字符串的指针。
  1. 获取标志位unsigned char flags = s[-1];* 这行代码从 SDS 字符串的末尾获取标志位。因为 SDS 字符串的实际数据存储在头结构之后,所以通过 s[-1] 可以访问到存储标志位的字节。
  2. 判断 SDS 类型并返回长度switch(flags&SDS_TYPE_MASK)*使用了一个位掩码 SDS_TYPE_MASK来提取标志位中的 SDS 类型信息。SDS_TYPE_MASK 的值通常是一个只有低三位设置为 1 的值(例如 0x07),这样可以将标志位中的类型字段提取出来。
  • switch 语句根据提取出来的类型字段的值来选择相应的代码分支,每个分支都返回相应类型 SDS 字符串的长度。
  • case SDS_TYPE_5:如果 SDS 类型是 5,则使用 SDS_TYPE_5_LEN(Flags) 来计算字符串的长度。这个宏通常会从标志位中提取出长度信息。
  • case SDS_TYPE_8case SDS_TYPE_16case SDS_TYPE_32case SDS_TYPE_64:对于其他类型的 SDS 字符串,通过调用 SDS_HDR(size, s) 宏来获取头结构的指针,然后返回头结构中的 len 字段的值,即字符串的实际长度。
  1. 默认返回值return 0;* 如果 switch 语句中没有匹配到任何类型,函数将返回 0。这通常表示输入的 s 不是一个有效的 SDS 字符串,或者它的类型字段的值不正确。

计算剩余容量

sdsavail 函数用于快速获取 SDS 字符串的剩余容量。该函数的实现基于 SDS 字符串的 alloc(总容量)和 len(已使用长度)字段。通过简单的数学运算 alloc - len,即可得到剩余容量。这种计算方式的时间复杂度为 O(1),意味着无论 SDS 字符串的长度如何,获取剩余容量的操作都是常数时间复杂度的,因此非常高效。

c

复制代码

static inline size_t sdsavail(const sds s) {
    unsigned char flags = s[-1];
    switch(flags&SDS_TYPE_MASK) {
        case SDS_TYPE_5: {
            return 0;
        }
        case SDS_TYPE_8: {
            SDS_HDR_VAR(8,s);
            return sh->alloc - sh->len;
        }
        case SDS_TYPE_16: {
            SDS_HDR_VAR(16,s);
            return sh->alloc - sh->len;
        }
        case SDS_TYPE_32: {
            SDS_HDR_VAR(32,s);
            return sh->alloc - sh->len;
        }
        case SDS_TYPE_64: {
            SDS_HDR_VAR(64,s);
            return sh->alloc - sh->len;
        }
    }
    return 0;
}

简而言之,sdsavail 函数提供了一种快速、简便的方法来查询 SDS 字符串当前剩余的可用空间,这对于需要动态调整字符串大小的应用来说非常有用。

至此,还有还有很多关于SDS源码的其他内容,由于篇幅过长,再次就不过多介绍,本次我们主要是将核心SDS的核心特细和原理以及结构,以及基本的核心方法进行介绍和说明,特别是针对于SDS和C字符串的区别和选择的问题,做了较为深入的介绍和分析。


作者推荐 |【Redis技术进阶之路】「原理系列开篇」揭秘高效存储模型与数据结构底层实现(SDS)(三)https://developer.aliyun.com/article/1471145

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
2月前
|
消息中间件 缓存 NoSQL
Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。
【10月更文挑战第4天】Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。随着数据增长,有时需要将 Redis 数据导出以进行分析、备份或迁移。本文详细介绍几种导出方法:1)使用 Redis 命令与重定向;2)利用 Redis 的 RDB 和 AOF 持久化功能;3)借助第三方工具如 `redis-dump`。每种方法均附有示例代码,帮助你轻松完成数据导出任务。无论数据量大小,总有一款适合你。
78 6
|
1天前
|
存储 消息中间件 监控
Redis Stream:实时数据流的处理与存储
通过上述分析和具体操作示例,您可以更好地理解和应用 Redis Stream,满足各种实时数据处理需求。
26 14
|
1月前
|
存储 NoSQL 算法
Redis分片集群中数据是怎么存储和读取的 ?
Redis集群采用哈希槽分区算法,共有16384个哈希槽,每个槽分配到不同的Redis节点上。数据操作时,通过CRC16算法对key计算并取模,确定其所属的槽和对应的节点,从而实现高效的数据存取。
49 13
|
1月前
|
存储 NoSQL Redis
【赵渝强老师】Redis的存储结构
Redis 默认配置包含 16 个数据库,通过 `databases` 参数设置。每个数据库编号从 0 开始,默认连接 0 号数据库,可通过 `SELECT &lt;dbid&gt;` 切换。Redis 的核心存储结构包括 `dict`、`expires` 等字段,用于处理键值和过期行为。添加键时需指定数据库信息。视频讲解和代码示例详见内容。
|
2月前
|
存储 安全 数据库
除了 HashMap,还有哪些数据结构可以实现键值对存储?
【10月更文挑战第11天】 除了`HashMap`,其他常见支持键值对存储的数据结构包括:`TreeMap`(基于红黑树,键有序)、`LinkedHashMap`(保留插入顺序)、`HashTable`(线程安全)、`B-Tree`和`B+Tree`(高效存储大量数据)、`SkipList`(通过跳跃指针提高查找效率)及`UnorderedMap`(类似`HashMap`)。选择合适的数据结构需根据排序、并发、存储和查找性能等需求。
|
2月前
|
设计模式 NoSQL 网络协议
大数据-48 Redis 通信协议原理RESP 事件处理机制原理 文件事件 时间事件 Reactor多路复用
大数据-48 Redis 通信协议原理RESP 事件处理机制原理 文件事件 时间事件 Reactor多路复用
43 2
|
2月前
|
存储 缓存 NoSQL
大数据-46 Redis 持久化 RDB AOF 配置参数 混合模式 具体原理 触发方式 优点与缺点
大数据-46 Redis 持久化 RDB AOF 配置参数 混合模式 具体原理 触发方式 优点与缺点
70 1
|
2月前
|
消息中间件 NoSQL Kafka
大数据-116 - Flink DataStream Sink 原理、概念、常见Sink类型 配置与使用 附带案例1:消费Kafka写到Redis
大数据-116 - Flink DataStream Sink 原理、概念、常见Sink类型 配置与使用 附带案例1:消费Kafka写到Redis
191 0
|
1月前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
172 9
|
1月前
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
30 1