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

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

【专栏简介】

随着数据需求的迅猛增长,持久化和数据查询技术的重要性日益凸显。关系型数据库已不再是唯一选择,数据的处理方式正变得日益多样化。在众多新兴的解决方案与工具中,Redis凭借其独特的优势脱颖而出。

【技术大纲】

为何Redis备受瞩目?原因在于其学习曲线平缓,短时间内便能对Redis有初步了解。同时,Redis在处理特定问题时展现出卓越的通用性,专注于其擅长的领域。深入了解Redis后,您将能够明确哪些任务适合由Redis承担,哪些则不适宜。这一经验对开发人员来说是一笔宝贵的财富。

在这个专栏中,我们将专注于Redis的6.2版本进行深入分析和介绍。Redis 6.2不仅是我个人特别偏爱的一个版本,而且在实际应用中也被广泛认为是稳定性和性能表现都相当出色的版本

【专栏目标】

本专栏深入浅出地传授Redis的基础知识,旨在助力读者掌握其核心概念与技能。深入剖析了Redis的大多数功能以及全部多机功能的实现原理,详细展示了这些功能的核心数据结构和关键算法思想。读者将能够快速且有效地理解Redis的内部构造和运作机制,这些知识将助力读者更好地运用Redis,提升其使用效率。

将聚焦于Redis的五大数据结构,深入剖析各种数据建模方法,并分享关键的管理细节与调试技巧。

【目标人群】

Redis技术进阶之路专栏:目标人群与受众对象,对于希望深入了解Redis实现原理底层细节的人群

1. Redis爱好者与社区成员

Redis技术有浓厚兴趣,经常参与社区讨论,希望深入研究Redis内部机制、性能优化和扩展性的读者。

2. 后端开发和系统架构师

在日常工作中经常使用Redis作为数据存储和缓存工具,他们在项目中需要利用Redis进行数据存储、缓存、消息队列等操作时,此专栏将为他们提供有力的技术支撑。

3. 计算机专业的本科生及研究生

对于学习计算机科学、软件工程、数据分析等相关专业的在校学生,以及对Redis技术感兴趣的教育工作者,此专栏可以作为他们的学习资料和教学参考。

无论是初学者还是资深专家,无论是从业者还是学生,只要对Redis技术感兴趣并希望深入了解其原理和实践,都是此专栏的目标人群和受众对象

让我们携手踏上学习Redis的旅程,探索其无尽的可能性!


SDS(简单动态字符串)

Redis没有采纳C语言传统的字符串表示方式,而是选择了一种独特而高效的方法,构建了一个名为简单动态字符串的抽象类型,并将其确立为自己的默认字符串实现。再深入分析SDS核心结构以及原理之前,我们认识一下C语言的字符串,分析一下为什么Redis不采用它?

C字符串

在C语言中,字符串的实现通常依赖于函数的形式。开发者可以使用char*字符数组来表示和操作字符串。为了简化字符串处理任务,C语言标准库提供了string.h头文件,其中定义了一系列字符串操作函数,如字符串连接、比较、查找、复制等。

C字符串存在的问题

字符串存储局限性

C语言使用长度为N+1的字符数组来存储长度为N的字符串。这种设计是为了确保字符串的最后一个元素始终是一个空字符('\0'),即ASCII码值为0的字符。它不仅是字符串结束的标记,还确保了字符串操作的安全性,防止了诸如越界访问等潜在问题,如下图所示:

由于C字符串的字符必须遵循特定的编码标准,如ASCII,并且在其内部结构中,除了字符串的自然终止点外,不允许出现空字符。这是因为C字符串的长度是通过遍历字符直至遇到空字符来确定的,如果在字符串中间出现空字符,程序会错误地将其当作字符串的结束,从而截断后续的数据。这一限制使得C字符串只能用来存储纯文本数据,而无法有效地处理二进制数据,如图片、音频、视频或压缩文件等。

不存储字符串长度信息

C语言中的字符串不直接存储其长度信息,要获取一个C字符串的长度,程序必须通过遍历整个字符串来逐一计数其中的字符。这个过程涉及从字符串的起始位置开始,逐个检查每个字符,直到遇到表示字符串终止的空字符('\0')为止。

操作C字符串的时间复杂度
  • 使用char*来操作字符串时,获取字符串长度的操作通常需要遍历整个字符串,因此其时间复杂度为O(N),其中N表示字符串的长度。
  • 当需要在字符串末尾追加内容时,也需要遍历整个字符串以找到其末尾位置,这个操作的时间复杂度也是O(N)。
案例说明

我们使用一个案例分析一下计算C字符串长度时的处理过程。传统上,C字符串的长度需要通过遍历整个字符串来逐个字符计数,直到遇到空字符作为字符串结束的标记。

上图展示了C语言程序在计算字符串长度时的处理过程。传统上,C字符串的长度需要通过遍历整个字符串来逐个字符计数。

引发的性能瓶颈

由于每个字符都需要被检查一次,这个操作的复杂度为O(n),其中n是字符串的长度。这意味着,随着字符串长度的增加,计算长度所需的时间也线性增长。因此,对于较长的字符串,这种计算方式可能会成为性能瓶颈,需要开发者在算法设计和性能优化时予以考虑。

出现缓冲区溢出问题

除了其获取字符串长度的复杂度较高之外,C字符串不记录自身长度还带来了一个关键的问题,即容易导致缓冲区溢出(buffer overflow)。缓冲区溢出是一种常见的安全漏洞,它发生在当程序试图将数据写入一个固定大小的缓冲区,而该数据的大小超过了缓冲区所能容纳的界限时。

strcat拼接引发缓冲区溢出

C标准库中的strcat函数为例,该函数用于将src字符串的内容拼接到dest字符串的末尾。如果dest字符串的缓冲区没有足够的空间来容纳src字符串的内容,就会发生缓冲区溢出。这种溢出可能导致程序崩溃,更糟糕的是,它还可能被恶意利用来执行任意代码,从而构成严重的安全威胁。

c

复制代码

char *strcat (char dest,const char *src);

考虑一个场景,在程序中存在两个紧密相邻的C字符串s1和s2,分别存储了字符串"hello"和"world",如下图所示。 在这个例子中,s1和s2在内存中紧密相连,这意味着它们的结束空字符('\0')后直接跟随的是下一个字符串的开始。在这种情况下,如果没有明确的分隔标识(如空字符),程序可能会错误地将s1和s2视为一个连续的字符串,从而导致逻辑错误或意外的行为。

如果一个程序员决定通过执行以下代码:

c

复制代码

strcat(s1, "t");

来将字符串 "t" 追加到已存在的 C 字符串 s1 的末尾,那么这里存在几个潜在的问题和风险。

因为strcat 函数不会自动检查 s1 是否有足够的空间来容纳新添加的字符串 "t"。如果 s1 的缓冲区没有足够的空间来容纳原始字符串和追加的字符串,就会发生缓冲区溢出到s2的缓冲空间中,导致s2保存的内容被意外地修改,如下图所示:

SDS的优势特点

节省数据内存分配

SDS的设计,使得Redis在处理字符串时,能够更加灵活和高效。与传统的C字符串相比,SDS提供了动态调整大小的功能,省去了频繁的内存分配和复制操作,从而显著提升了性能。

解决终止符的问题

C语言中字符串是通过char*类型的字符数组来表示的,这些数组以空字符('\0')作为字符串的终止符。如果字符串中间包含空字符,它将被错误地解释为字符串的结束,从而导致字符串无法被正确表示。

c

复制代码

#include "stdio.h"
  #include "string.h"
  int main(void) {
      char *t1 = "hello\0world";
      char *t2 = "helloworld\0";
      printf("%lu\n", strlen(t1));
      printf("%lu\n", strlen(t2));
  }
  输出结果是 5 和 10。

其中底层的字符存储模式如下所示: 为了确保Redis能够灵活适应各种不同的使用场景,SDS被设计为二进制安全的。所有的SDS API在处理存储在buf数组中的数据时,都会以二进制的方式进行处理。程序不会对这些数据施加任何限制、过滤或假设,确保数据在写入时和读取时保持一致。

这也是为什么SDS的buf属性称为字节数组的原因,因为Redis并不是使用这个数组来保存字符,而是用它来保存一系列原始的二进制数据。这种设计使得SDS能够轻松应对各种复杂的数据格式,包括那些使用特殊字符或编码的数据。

内存溢出控制

C语言的字符串是静态分配的,当追加的内容导致字符串长度超过原分配的空间时,就可能发生溢出错误或无法追加的情况。这是因为SDS在内部维护了字符串的长度信息,并且使用了动态内存分配来管理字符串空间。

sdscat拼接案例分析

SDS的sdscat函数为例,该函数旨在将一个C字符串追加到给定SDS所存储的字符串的末尾。然而,与C标准库中的strcat函数不同,sdscat在执行拼接操作之前,会先对给定SDS的可用空间进行详尽的检查。如果SDS的当前空间不足以容纳即将追加的C字符串,sdscat会智能地扩展SDS的空间,确保有足够的容量来容纳拼接后的新字符串。只有当空间扩展完成后,sdscat才会执行实际的拼接操作。

空间分配策略进行了优化,从而彻底消除了缓冲区溢出的风险。当使用SDS API对SDS进行修改时,该API会预先检查SDS当前的空间是否满足修改所需的条件。如果SDS的空间不足以支持所需的修改,API会自动执行空间扩展操作,将SDS的大小调整至足以执行修改所需的大小。只有在确保空间足够后,API才会执行实际的修改操作。

提升操作效率

Redis所采用的简单动态字符串(SDS)则在这些方面进行了优化,SDS不仅能够在O(1)的时间复杂度内获取字符串长度,还能在O(1)的时间复杂度内完成字符串的追加操作。

SDS通过在其内部结构中包含一个len属性来直接记录字符串的长度信息。因此,在SDS中,获取字符串长度的操作不再需要遍历整个字符串,而是直接读取len属性的值,这使得获取SDS长度的复杂度降低到了O(1),即无论SDS的长度如何,获取其长度所需的时间都是恒定的。

因此,可以说SDS是Redis在处理字符串数据时的一项创新举措,它使得Redis能够以更加高效和灵活的方式处理字符串,从而为用户提供了更加稳定和高效的数据存储和访问体验。


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

相关文章
|
存储 缓存 NoSQL
Redis 服务器全方位介绍:从入门到核心原理
Redis是一款高性能内存键值数据库,支持字符串、哈希、列表等多种数据结构,广泛用于缓存、会话存储、排行榜及消息队列。其单线程事件循环架构保障高并发与低延迟,结合RDB和AOF持久化机制兼顾性能与数据安全。通过主从复制、哨兵及集群模式实现高可用与横向扩展,适用于现代应用的多样化场景。合理配置与优化可显著提升系统性能与稳定性。
598 0
|
5月前
|
存储 缓存 监控
Redis分区的核心原理与应用实践
Redis分区通过将数据分散存储于多个节点,提升系统处理高并发与大规模数据的能力。本文详解分区原理、策略及应用实践,涵盖哈希、范围、一致性哈希等分片方式,分析其适用场景与性能优势,并探讨电商秒杀、物联网等典型用例,为构建高性能、可扩展的Redis集群提供参考。
290 0
|
12月前
|
消息中间件 缓存 NoSQL
Redis原理—5.性能和使用总结
本文详细探讨了Redis的阻塞原因、性能优化、缓存相关问题及数据库与缓存的一致性问题。同时还列举了不同缓存操作方案下的并发情况,帮助读者理解并选择合适的缓存管理策略。最终得出结论,在实际应用中应尽量采用“先更新数据库再删除缓存”的方案,并结合异步重试机制来保证数据的一致性和系统的高性能。
Redis原理—5.性能和使用总结
|
12月前
|
缓存 NoSQL Redis
Redis原理—2.单机数据库的实现
本文概述了Redis数据库的核心结构和操作机制。
Redis原理—2.单机数据库的实现
|
11月前
|
缓存 NoSQL 中间件
Redis的线程模型
Redis采用单线程模型确保操作的原子性,每次只执行一个操作,避免并发冲突。它通过MULTI/EXEC事务机制、Lua脚本和复合指令(如MSET、GETSET等)保证多个操作要么全成功,要么全失败,确保数据一致性。Redis事务在EXEC前失败则不执行任何操作,EXEC后失败不影响其他操作。Pipeline虽高效但不具备原子性,适合非热点时段的数据调整。Redis 7引入Function功能,支持函数复用,简化复杂业务逻辑。总结来说,Redis的单线程模型简单高效,适用于高并发场景,但仍需合理选择指令执行方式以发挥其性能优势。
286 6
|
12月前
|
存储 缓存 NoSQL
Redis原理—4.核心原理摘要
Redis 是一个基于内存的高性能NoSQL数据库,支持分布式集群和持久化。其网络通信模型采用多路复用监听与文件事件机制,通过单线程串行化处理大量并发请求,确保高效运行。本文主要简单介绍了 Redis 的核心特性。
|
12月前
|
缓存 NoSQL Redis
Redis原理—3.复制、哨兵和集群
详细介绍了Redis的复制原理、哨兵原理和集群原理。
|
12月前
|
运维 NoSQL 算法
【📕分布式锁通关指南 04】redis分布式锁的细节问题以及RedLock算法原理
本文深入探讨了基于Redis实现分布式锁时遇到的细节问题及解决方案。首先,针对锁续期问题,提出了通过独立服务、获取锁进程自己续期和异步线程三种方式,并详细介绍了如何利用Lua脚本和守护线程实现自动续期。接着,解决了锁阻塞问题,引入了带超时时间的`tryLock`机制,确保在高并发场景下不会无限等待锁。最后,作为知识扩展,讲解了RedLock算法原理及其在实际业务中的局限性。文章强调,在并发量不高的场景中手写分布式锁可行,但推荐使用更成熟的Redisson框架来实现分布式锁,以保证系统的稳定性和可靠性。
785 0
【📕分布式锁通关指南 04】redis分布式锁的细节问题以及RedLock算法原理
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
346 59
|
8月前
|
编译器 C语言 C++
栈区的非法访问导致的死循环(x64)
这段内容主要分析了一段C语言代码在VS2022中形成死循环的原因,涉及栈区内存布局和数组越界问题。代码中`arr[15]`越界访问,修改了变量`i`的值,导致`for`循环条件始终为真,形成死循环。原因是VS2022栈区从低地址到高地址分配内存,`arr`数组与`i`相邻,`arr[15]`恰好覆盖`i`的地址。而在VS2019中,栈区先分配高地址再分配低地址,因此相同代码表现不同。这说明编译器对栈区内存分配顺序的实现差异会导致程序行为不一致,需避免数组越界以确保代码健壮性。
172 0
栈区的非法访问导致的死循环(x64)