Redis | 数据结构(02)SDS
一、键值对数据库是怎么实现的?
在开始讲数据结构之前,先给介绍下 Redis 是怎样实现键值对(key-value)数据库的。
Redis 的键值对中的 key 就是字符串对象,而 value 可以是字符串对象,也可以是集合数据类型的对象,比如 List 对象、Hash 对象、Set 对象和 Zset 对象。
举个例子,我这里列出几种 Redis 新增键值对的命令:
> SET name "mingjing"
OK
> HSET person name "mingjing" age 18
0
> RPUSH stu "mingjing" "huahua"
(integer)
这些命令代表着:
● 第一条命令:name 是一个字符串键,因为键的值是一个字符串对象;
● 第二条命令:person 是一个哈希表键,因为键的值是一个包含两个键值对的哈希表对象;
● 第三条命令:stu 是一个列表键,因为键的值是一个包含两个元素的列表对象;
1.1 这些键值对是如何保存在 Redis 中的呢?
Redis 是使用了一个「哈希表」保存所有键值对,哈希表的最大好处就是让我们可以用 O(1)
的时间复杂度来快速查找到键值对。哈希表其实就是一个数组,数组中的元素叫做哈希桶。
1.2 Redis 的哈希桶是怎么保存键值对数据的呢?
哈希桶存放的是指向键值对数据的指针(dictEntry*),这样通过指针就能找到键值对数据,然后因为键值对的值可以保存字符串对象和集合数据类型的对象,所以键值对的数据结构中并不是直接保存值本身,而是保存了 void * key
和 void * value
指针,分别指向了实际的键对象和值对象,这样一来,即使值是集合数据,也可以通过 void * value
指针找到。如下图:
这些数据结构的内部细节,我先不展开讲,后面在讲哈希表数据结构的时候,在详细的说说,因为用到的数据结构是一样的。这里先大概说下图中涉及到的数据结构的名字和用途:
● redisDb 结构,表示 Redis 数据库的结构,结构体里存放了指向了 dict 结构的指针;
● dict 结构,结构体里存放了 2 个哈希表,正常情况下都是用「哈希表1」,「哈希表2」只有在 rehash 的时候才用,具体什么是 rehash,我在本文的哈希表数据结构会讲;
● ditctht 结构,表示哈希表的结构,结构里存放了哈希表数组,数组中的每个元素都是指向一个哈希表节点结构(dictEntry)的指针;
● dictEntry 结构,表示哈希表节点的结构,结构里存放了 void * key
和 void * value
指针, key 指向的是 String 对象,而 value 则可以指向 String 对象,也可以指向集合类型的对象,比如 List 对象、Hash 对象、Set 对象和 Zset 对象。
特别说明下,void * key
和 void * value
指针指向的是 Redis 对象,Redis 中的每个对象都由 redisObject 结构表示,如下图:
对象结构里包含的成员变量:
● type,标识该对象是什么类型的对象(String 对象、 List 对象、Hash 对象、Set 对象和 Zset 对象);
● encoding,标识该对象使用了哪种底层的数据结构;
● ptr,指向底层数据结构的指针。
所以Redis 键值对数据库的全景图如下,你就能清晰知道 Redis 对象和数据结构的关系了:
接下里,就好好聊一下底层数据结构!
二、SDS简洁
字符串在 Redis 中是很常用的,键值对中的键是字符串类型,值有时也是字符串类型。
Redis 是用 C 语言实现的,但是它没有直接使用 C 语言的 char
字符数组来实现字符串,而是自己封装了一个名为简单动态字符串(simple dynamic string,SDS) 的数据结构来表示字符串,也就是 Redis 的 String 数据类型的底层数据结构是 SDS。
既然 Redis 设计了 SDS 结构来表示字符串,肯定是 C 语言的 char
字符数组存在一些缺陷。
要了解这一点,得先来看看 char
字符数组的结构。
2.1 Redis为什么不直接使用C 语言字符串
C 语言的字符串其实就是一个字符数组,即数组中每个元素是字符串中的一个字符。
比如,下图就是字符串“mingjing”的 char
字符数组的结构:
char* name = “mingjing”
m | i | n | g | j | i | n | g | \0 |
---|
在 C 语言里,对字符串操作时,char * 指针只是指向字符数组的起始位置,而字符数组的结尾位置就用“0”表示,意思是指字符串的结束。
因此,C 语言标准库中的字符串操作函数就通过判断字符是不是 “0” 来决定要不要停止操作,如果当前字符不是 “0” ,说明字符串还没结束,可以继续操作,如果当前字符是 “0” 是则说明字符串结束了,就要停止操作。
C 语言字符串用 “0” 字符作为结尾标记有个缺陷。假设有个字符串中有个 “0” 字符,这时在操作这个字符串时就会提早结束,
因此,除了字符串的末尾之外,字符串里面不能含有 “0” 字符,否则最先被程序读入的 “0” 字符将被误认为是字符串结尾,这个限制使得 C 语言的字符串只能保存文本数据,不能保存像图片、音频、视频文化这样的二进制数据(这也是一个可以改进的地方)
另外, C 语言标准库中字符串的操作函数是很不安全的,对程序员很不友好,稍微一不注意,就会导致缓冲区溢出。
举个例子,strcat 函数是可以将两个字符串拼接在一起。
//将 src 字符串拼接到 dest 字符串后面
char*strcat(char*dest,constchar* src);
C 语言的字符串是不会记录自身的缓冲区大小的,所以 strcat 函数假定程序员在执行这个函数时,已经为 dest 分配了足够多的内存,可以容纳 src 字符串中的所有内容,而一旦这个假定不成立,就会发生缓冲区溢出将可能会造成程序运行终止,(这是一个可以改进的地方)。
而且,strcat 函数和 strlen 函数类似,时间复杂度也很高,也都需要先通过遍历字符串才能得到目标字符串的末尾。然后对于 strcat 函数来说,还要再遍历源字符串才能完成追加,对字符串的操作效率不高。
好了, 通过以上的分析,我们可以得知 C 语言的字符串不足之处以及可以改进的地方:
● 获取字符串长度的时间复杂度为 O(N);
● 字符串的结尾是以 “0” 字符标识,字符串里面不能包含有 “0” 字符,因此不能保存二进制数据;
● 字符串操作函数不高效且不安全,比如有缓冲区溢出的风险,有可能会造成程序运行终止;
Redis 实现的 SDS 的结构就把上面这些问题解决了,接下来我们一起看看 Redis 是如何解决的。
三、 SDS 结构设计
下图就是 Redis 5.0 的 SDS 的数据结构:
结构中的每个成员变量分别介绍下:
● len,记录了字符串长度。这样获取字符串长度的时候,只需要返回这个成员变量值就行,时间复杂度只需要 O(1)。
● alloc,分配给字符数组的空间长度。这样在修改字符串的时候,可以通过 alloc - len 计算出剩余的空间大小,可以用来判断空间是否满足修改需求,如果不满足的话,就会自动将 SDS 的空间扩展至执行修改所需的大小,然后才执行实际的修改操作,所以使用 SDS 既不需要手动修改 SDS 的空间大小,也不会出现前面所说的缓冲区溢出的问题。
● flags,用来表示不同类型的 SDS。一共设计了 5 种类型,分别是 sdshdr5、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64,后面在说明区别之处。
● buf[],字符数组,用来保存实际数据。不仅可以保存字符串,也可以保存二进制数据。
总的来说,Redis 的 SDS 结构在原本字符数组之上,增加了三个元数据:len、alloc、flags,用来解决 C 语言字符串的缺陷。
3.1 O(1)复杂度获取字符串长度
C 语言的字符串长度获取 strlen 函数,需要通过遍历的方式来统计字符串长度,时间复杂度是 O(N)。
而 Redis 的 SDS 结构因为加入了 len 成员变量,那么获取字符串长度的时候,直接返回这个成员变量的值就行,所以复杂度只有 O(1)。
3.2 二进制安全
因为 SDS 不需要用 “0” 字符来标识字符串结尾了,而是有个专门的 len 成员变量来记录长度,所以可存储包含 “0” 的数据。但是 SDS 为了兼容部分 C 语言标准库的函数, SDS 字符串结尾还是会加上 “0” 字符。
因此, SDS 的 API 都是以处理二进制的方式来处理 SDS 存放在 buf[] 里的数据,程序不会对其中的数据做任何限制,数据写入的时候时什么样的,它被读取时就是什么样的。
通过使用二进制安全的 SDS,而不是 C 字符串,使得 Redis 不仅可以保存文本数据,也可以保存任意格式的二进制数据。
3.3 不会发生缓冲区溢出
C 语言的字符串标准库提供的字符串操作函数,大多数(比如 strcat 追加字符串函数)都是不安全的,因为这些函数把缓冲区大小是否满足操作需求的工作交由开发者来保证,程序内部并不会判断缓冲区大小是否足够用,当发生了缓冲区溢出就有可能造成程序异常结束。
所以,Redis 的 SDS 结构里引入了 alloc 和 len 成员变量,这样 SDS API 通过 alloc - len 计算,可以算出剩余可用的空间大小,这样在对字符串做修改操作的时候,就可以由程序内部判断缓冲区大小是否足够用。
而且,当判断出缓冲区大小不够用时,Redis 会自动将扩大 SDS 的空间大小,以满足修改所需的大小。
SDS 扩容的规则代码如下:
hisds hi_sdsMakeRoomFor(hisds s,size_t addlen)
{......// s目前的剩余空间已足够,无需扩展,直接返回if(avail >= addlen)return s;//获取目前s的长度len =hi_sdslen(s);sh =(char*)s -hi_sdsHdrSize(oldtype);//扩展之后 s 至少需要的长度newlen =(len + addlen);//根据新长度,为s分配新空间所需要的大小if(newlen < HI_SDS_MAX_PREALLOC)//新长度<HI_SDS_MAX_PREALLOC 则分配所需空间*2的空间newlen *=2;else//否则,分配长度为目前长度+1MBnewlen += HI_SDS_MAX_PREALLOC;...
}
● 如果所需的 sds 长度小于 1 MB,那么最后的扩容是按照翻倍扩容来执行的,即 2 倍的newlen
● 如果所需的 sds 长度超过 1 MB,那么最后的扩容长度应该是 newlen + 1MB。
在扩容 SDS 空间之前,SDS API 会优先检查未使用空间是否足够,如果不够的话,API 不仅会为 SDS 分配修改所必须要的空间,还会给 SDS 分配额外的「未使用空间」。
这样的好处是,下次在操作 SDS 时,如果 SDS 空间够的话,API 就会直接使用「未使用空间」,而无须执行内存分配,有效的减少内存分配次数。
所以,使用 SDS 即不需要手动修改 SDS 的空间大小,也不会出现缓冲区溢出的问题。
相关文章:

Redis | 数据结构(02)SDS
一、键值对数据库是怎么实现的? 在开始讲数据结构之前,先给介绍下 Redis 是怎样实现键值对(key-value)数据库的。 Redis 的键值对中的 key 就是字符串对象,而 value 可以是字符串对象,也可以是集合数据类型…...

Linux C语言开发-D7D8运算符
算术运算符:-*/%,浮点数可以参与除法运算,但不能参与取余运算 a%b:表示取模或取余 关系运算符:<,>,>,<,,! 逻辑运算符:!,&&,|| &&,||逻辑运算符是从左到右,依次运算&#…...

redis 配置主从复制,哨兵模式案例
哨兵(Sentinel)模式 1 . 什么是哨兵模式? 反客为主的自动版,能够自动监控master是否发生故障,如果故障了会根据投票数从slave中挑选一个 作为master,其他的slave会自动转向同步新的master,实现故障自动转义 2 . 原理…...

Python---练习:使用for循环实现用户名+密码认证
案例: 用for循环实现用户登录 ① 输入用户名和密码 ② 判断用户名和密码是否正确(usernamelaowang,passwordlw123) ③ 登录仅有三次机会,超过3次会报错 思考: 用户登陆情况有3种: ① 用户名错误(此时…...
react中使用jquery 语法
react中使用jquery 语法 npm install jquery引入 import $ from ‘jquery’ import React from react; import ./css/App.css import { Button } from antd; import $ from jquerylet slider_img [https://cdn.jsdelivr.net/gh/xaoxuu/cdn-wallpaper/abstract/41F215B9-261F…...

服务器中了360后缀勒索病毒怎么解决,勒索病毒解密,数据恢复
近期,网络上的各种病毒都比较猖獗,而其中较为明显的就是360后缀勒索病毒,从这个月开始云天数据恢复中心接到很多企业的求助,企业的服务器遭到了360后缀勒索病毒的攻击,通过给用户的服务器检测与加密病毒的分析…...

使用字节流读取文件中的数据的几种方式
public class FileReader02_ {public static void main(String[] args) {}Testpublic void m1() {String filePath "e:\\hello.txt";FileReader fileReader null;int date0;try {fileReader new FileReader(filePath);//循环读取 使用readwhile ((datefileReader.…...

Android WMS——概述(一)
Android 中的 WMS 指的是 Window Manager Service(窗口管理服务)。WMS 是 Android 系统中的核心服务,主要分为四大部分,分别是窗口管理,窗口动画,输入系统中转站和 Surface 管理 。负责管理应用程序窗口的创建、移动、调整大小和显示等操作。 一、功能简介 WMS 的职责可…...

Node编写获取用户信息接口
目录 前言 初始化路由模块 使用postman发送get获取用户信息请求 初始化路由处理函数模块 获取用户基本信息 前言 在前两篇文章中已经介绍了如何编写用户注册接口以及用户登录接口,这篇文章介绍如何获取用户信息,本篇文章建立在Node编写用户登录接口…...
【从0到1设计一个网关】自研网关的设计要点以及架构设计
文章目录 请求的流程架构设计设计要点项目架构流程设计源码地址: 源码地址 请求的流程 一个HTTP请求发送到网关并完成整个生命周期通常包括以下步骤: 客户端请求: 请求始于客户端,客户端通过HTTP请求(例如GET、POST等)发送请求到API网关的入口点。 API网关接收: API…...

论文-分布式-分布式计算|容错-分布式控制下的自稳定系统
参考文献Self-stabilizing systems in spite of distributed control可以把松散耦合的 循环序列过程 间的同步任务,看成是要保持一个这样的不变性:“系统要处于一种合法状态”因此每个进程在运行每一个可能会改变不变性的步骤之前都要先检查一下是可以执…...
C#压缩图片的方法
/// <summary> /// 图片压缩 /// </summary> /// <param name"imagePath">图片文件路径</param> /// <param name"targetFolder">保存文件夹</param> /// <param name"quality">压缩质量</param&g…...
安装 fcitx + 搜狗/谷歌输入法 之后导致 死机,重启后黑屏只有鼠标可以移动
一般的原因就是 : fcitx 导致的问题 方法就是 先卸载搜狗,再卸载fcitx 解决办法: 首先:ctrlaltF6 进入命令行界面,如果进不去就 ctrlaltF2 接下来执行: sudo apt-get remove sogoupinyin sudo apt-get …...

Maven项目转为SpringBoot项目
Maven项目转为SpringBoot项目 前言创建一个maven项目前的软件的一些通用设置Maven仓库的设置其他的设置字符编码编译器注解支持 创建的Maven项目修改为Spring Boot项目修改pom.xml文件修改启动类-Main新建WAR包所需的类 添加核心配置文件 测试的控制器最后整个项目的目录结构
C语言之预处理
目录 前言 宏定义define的用法 文件包含include的用法 条件编译的用法 其他预处理命令 练习题 练习一 练习二 练习三 前言 预处理命令可以改变程序设计环境,提高编程效率,它们并不是C语言本身的组成部分,不能直接对它们进行编译&am…...

css步骤条
html 代码以及样式 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>css步骤条</title><style>.steps {display: flex;justify-content: space-between;padding: 0;margin: 20px 10px;lis…...
[Hive] 常见函数
文章目录 字符串函数数值函数随机函数日期和时间函数字符串转时间 聚合函数数组函数结构体函数数组函数映射函数 map正则处理JSON 字符串函数 CONCAT(string1, string2, …):将多个字符串连接成一个字符串。 LENGTH(string):返回字符串的长度。 LOWER…...

Mac用NTFS文件夹读写NTFS硬盘 NTFS能复制多大的文件
Mac作为一款备受欢迎的计算机操作系统,具备了许多令人惊叹的功能和特性。然而,对于一些Mac用户来说,使用NTFS格式的硬盘可能存在一些疑问。他们可能想知道Mac是否能够读写NTFS格式的硬盘,以及NTFS格式的硬盘是否有文件大小的限制。…...

【unity3D】Scroll Rect组件—制作下滑列表
💗 未来的游戏开发程序媛,现在的努力学习菜鸡 💦本专栏是我关于游戏开发的学习笔记 🈶本篇是unity的Scroll Rect组件 Scroll Rect组件 基础知识详细说明案例演示——制作一个简单的下滑框扩展 介绍:Scroll Rect组件是用…...

网络扫描与网络监听
前言:前文给大家介绍了网络安全相关方面的基础知识体系,以及什么是黑客,本篇文章笔者就给大家带来“黑客攻击五部曲”中的网络扫描和网络监听 目录 黑客攻击五部曲 网络扫描 按扫描策略分类 按照扫描方式分类 被动式策略 系统用户扫描 …...

网络编程(Modbus进阶)
思维导图 Modbus RTU(先学一点理论) 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…...
HTML 语义化
目录 HTML 语义化HTML5 新特性HTML 语义化的好处语义化标签的使用场景最佳实践 HTML 语义化 HTML5 新特性 标准答案: 语义化标签: <header>:页头<nav>:导航<main>:主要内容<article>&#x…...

Linux 文件类型,目录与路径,文件与目录管理
文件类型 后面的字符表示文件类型标志 普通文件:-(纯文本文件,二进制文件,数据格式文件) 如文本文件、图片、程序文件等。 目录文件:d(directory) 用来存放其他文件或子目录。 设备…...

python打卡day49
知识点回顾: 通道注意力模块复习空间注意力模块CBAM的定义 作业:尝试对今天的模型检查参数数目,并用tensorboard查看训练过程 import torch import torch.nn as nn# 定义通道注意力 class ChannelAttention(nn.Module):def __init__(self,…...

【JavaEE】-- HTTP
1. HTTP是什么? HTTP(全称为"超文本传输协议")是一种应用非常广泛的应用层协议,HTTP是基于TCP协议的一种应用层协议。 应用层协议:是计算机网络协议栈中最高层的协议,它定义了运行在不同主机上…...
线程同步:确保多线程程序的安全与高效!
全文目录: 开篇语前序前言第一部分:线程同步的概念与问题1.1 线程同步的概念1.2 线程同步的问题1.3 线程同步的解决方案 第二部分:synchronized关键字的使用2.1 使用 synchronized修饰方法2.2 使用 synchronized修饰代码块 第三部分ÿ…...
在四层代理中还原真实客户端ngx_stream_realip_module
一、模块原理与价值 PROXY Protocol 回溯 第三方负载均衡(如 HAProxy、AWS NLB、阿里 SLB)发起上游连接时,将真实客户端 IP/Port 写入 PROXY Protocol v1/v2 头。Stream 层接收到头部后,ngx_stream_realip_module 从中提取原始信息…...

DBAPI如何优雅的获取单条数据
API如何优雅的获取单条数据 案例一 对于查询类API,查询的是单条数据,比如根据主键ID查询用户信息,sql如下: select id, name, age from user where id #{id}API默认返回的数据格式是多条的,如下: {&qu…...
大模型多显卡多服务器并行计算方法与实践指南
一、分布式训练概述 大规模语言模型的训练通常需要分布式计算技术,以解决单机资源不足的问题。分布式训练主要分为两种模式: 数据并行:将数据分片到不同设备,每个设备拥有完整的模型副本 模型并行:将模型分割到不同设备,每个设备处理部分模型计算 现代大模型训练通常结合…...

NFT模式:数字资产确权与链游经济系统构建
NFT模式:数字资产确权与链游经济系统构建 ——从技术架构到可持续生态的范式革命 一、确权技术革新:构建可信数字资产基石 1. 区块链底层架构的进化 跨链互操作协议:基于LayerZero协议实现以太坊、Solana等公链资产互通,通过零知…...