Redis数据结构之SDS

news2024/10/7 19:30:00

前言

字符串在 Redis 中的应用场景十分广泛,所有的键都是字符串类型,值也可能是字符串类型。
比如电商系统用 Redis 缓存商品信息,可以把商品 ID 作为键,商品信息序列化为 JSON 后作为值写入:

SET item:1001 '{"title":"苹果","price":99}'

Redis 是用 C 语言开发的,在 C 语言中,字符串可以用字符数组来表示,但是 Redis 并没有直接使用原生的字符数组,而是自定义了一个叫 Simple Dynamic String(简单动态字符串,缩写 SDS)的数据结构,它相较于原生字符数组有哪些优势呢?

字符数组

在 C 语言中,字符串可以用字符数组来表示:

char *s = "redis";

字符数组就是一块连续的物理内存,依次存放每个字符,为了标记字符串结束,会在末尾加上一个 ‘\0’ 零字节字符代表结束。
image.png

字符数组存在的问题有:

  • 操作效率低下
  • 缓冲区溢出风险
  • 二进制不安全

操作效率低
变量 s 是个指针,指向了字符数组的起始位置,除此之外再没有其它元数据来描述这个字符串了。所以,要获取字符串的长度该怎么做呢?只能是从起始位置开始遍历,直到扫描到 ‘\0’ 字符,然后返回统计的字符数量,时间复杂度是 O(N),这是 Redis 不能接受的。

缓冲区溢出风险
C 语言的字符数组是一片连续的内存空间,而且大小也是连续的。如果向一个字符数组写入超过其容量的数据,就会导致缓冲区溢出,破坏相邻的内存区域的数据,指针操作是危险的。

二进制不安全
C 的字符数组是二进制不安全的,例如 ‘\0’ 就无法存储,因为它被作为字符串的结束标记,如下示例,字符串 s2 将被截断为 ‘red’。

int main() {
    char *s1 = "redis";
    char *s2 = "red\0is";
    printf("%lu %s\n", strlen(s1), s1);
    printf("%lu %s\n", strlen(s2), s2);
    return 0;
}
// 输出
// 5 redis
// 3 red

sdshdr

为了尽量复用 C 标准库中字符串的操作函数,Redis 仍然使用字符数组来保存实际的数据。但是,为了更加高效的管理字符串,Redis 新增了头部结构,自定义了 sdshdr 数据结构。

sdshdr 的通用结构

属性长度说明
len1/2/4/8 字节已使用的长度
alloc1/2/4/8 字节分配的长度
flags1 字节3 Bit 表示类型,5 Bit 未使用
buf-字符数组缓冲区

sdshdr 细分后有五种类型:

struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* used */
    uint8_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; /* used */
    uint32_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* used */
    uint64_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
数据结构头部占用空间说明
sdshdr51 字节存储长度小于 32 的字符串
sdshdr83 字节存储长度小于 2^8 的字符串
sdshdr165 字节存储长度小于 2^16 的字符串
sdshdr329 字节存储长度小于 2^32 的字符串
sdshdr6417 字节存储长度小于 2^64 的字符串

这五种类型结构一样,只是能保存的字符串长度不一样,Redis 为什么要费这么大劲,设计这么多的类型呢?
这是因为相较于 C 的原生字符数组,sds 的头部毕竟是要占用额外的内存空间,所以能省一点是一点。在大多数场景下,存储的都是短字符串,其实是没有必要用 int 存储长度的,这也体现了 Redis 节省内存的设计哲学。

除此之外,Redis 还使用专门的编译优化来节省内存空间,核心是:

__attribute__ ((__packed__))

默认情况下,编译器会按照 8 字节对齐的方式给变量分配内存,即使变量不需要 8 字节的空间,编译器也会分配 8 字节,这样就会造成浪费。而加了这个指令,意思就是告诉编译器,不要采用紧凑的方式分配内存,变量实际占用多少就分配多少,再一次体现了 Redis 节省内存的设计哲学。

问题:Redis 是怎么判断 sds 对象类型的?

sds 指针指向的并不是整个对象的起始位置,而是 buf 的起始位置,这样只要向左移一位 sds[-1] 就可以读取到 flags 进行判断了。C 的指针很危险,但不可否认,也很高效。

再来看看 sds 是如何解决 C 里面字符数组的缺陷的。
操作效率低下

sdshdr 使用单独的 len 属性来记录字符串的长度,所以 sds 获取字符串长度的时间复杂度是 O(1)。

缓冲区溢出风险

sds 会在运行时自动扩缩容,这避免了 C 字符数组操作不当导致的缓冲区溢出问题。因为头部已经有属性来记录字符串的长度和缓冲区总大小了,所以在做追加操作时,可以提前判断是否需要扩容。

二进制不安全

sds 单独用属性来记录字符串的长度,所以不需要 ‘\0’ 来做结束标记了,所以 sds 可以存储任意的二进制数据,它是二进制安全的。

除了解决 C 原生字符数组的一些问题,sds 还有一些特性:

  • 自动扩容

sds 相当于是一个动态的字符数组,当缓冲区不足以容纳写入的数据量时会自动扩容,当数据减小到一定量时又会自动缩容释放内存。

if (newlen < SDS_MAX_PREALLOC)
    newlen *= 2;
else
    newlen += SDS_MAX_PREALLOC;

扩容策略:

  1. 新的长度小于 1MB 时按照 2 倍扩容
  2. 新的长度大于等于 1MB 时按照 1MB 步长扩容
  • 内存预分配

sds 分配的缓冲区大小不会恰好容纳你要写入的数据,而是会预先多分配一些,这样在有新数据写入时,不必再重复申请内存空间。

  • 惰性删除

sds 字符串缩小后不会立即释放内存,而是会等待留着下次扩容时继续使用,还是避免频繁申请内存,换取字符串 追加 操作的性能。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1115845.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

使用python自动化操作如何使用subprocess,mac如何查看软件安装路径

使用下面这种方法实现需要配置全局的环境变量&#xff0c;很麻烦 import subprocessdef open_wps_new_doc():try:# 打开WPS应用程序subprocess.Popen(wps)# 等待一段时间&#xff0c;确保WPS完全打开time.sleep(2)# 发送快捷键组合&#xff0c;新建一个Word文档pyautogui.hotk…

linux进阶(3)

课程链接 CH10-2-Apache的其他用途_哔哩哔哩_bilibili scp不够好,因为他需要知道服务器上具体的一个目录

MySQL -- 数据库基础

MySQL – 数据库基础 文章目录 MySQL -- 数据库基础一、基础知识1.什么是数据库2.连接服务器3.服务器、数据库、表的关系3.MySQL架构4.SQL分类5.存储引擎 一、基础知识 1.什么是数据库 文件存储数据有以下几个缺点&#xff1a; 文件的安全性问题文件不利于数据查询和管理文件…

【ARM AMBA5 CHI 入门 12.2 -- CHI 协议层详细介绍 】

文章目录 1 协议层1.1 协议层传输通道1.2 域段1.2.1 ID域段1.2.2 其他关键域段1.2.2.1 Address1.2.2.2 Secure bit1.2.2.3 Memory Attributes1.2.2.4 Transaction attribute combinations 1.4.1 Transaction 路由1.4.2 SAM 介绍1.4.3 Node ID 1.5 节点间数据怎么传输的呢&#…

jdk11的HttpClient

我们都知道在jdk11之前都在用okhttp或者org.apache.httpcomponents 其实早在jdk9的时候这个方案就在孵化中 上面的截图来自openjdk的官网&#xff0c;注&#xff1a;openjdk是个开源项目&#xff0c;不存在侵权现象 这是openjdk的官网&#xff1a;JEP 110: HTTP/2 Client (In…

vue 插槽 作用域插槽

vue 插槽 作用域插槽 **创建 工程&#xff1a; H:\java_work\java_springboot\vue_study ctrl按住不放 右键 悬着 powershell H:\java_work\java_springboot\js_study\Vue2_3入门到实战-配套资料\01-随堂代码素材\day05\准备代码\10-插槽-作用域插槽 vue --version vue crea…

安科瑞关于红外测温技术在变电站运维中的应用

安科瑞 崔丽洁 红外测温技术 特点 工作中的输变电机械设备由于电流热效应产生了红外线照射效应&#xff0c;从而在电气设备表层形成了相应的高温场&#xff0c;而红外线测温高温技术则透过吸取这些自高温场发出的红外线照射热能&#xff0c;并透过电流效应以及放大器和A/D转换器…

Linux - 还不懂 gdb 调试器?(调试软件)

前言 当前&#xff0c;我们可以使用 make/makefile 来程序化执行代码文件&#xff1b;可以使用 gcc/g 等编译器来编译代码&#xff1b;可以使用 vim 编辑器来编写代码&#xff1b;其实在 Linux 当中还有一个工具&#xff0c;可以实现调试工作&#xff0c;这个工具就是 -- gdb。…

RK3568驱动指南|第七期-设备树-第57章 实例分析:中断

瑞芯微RK3568芯片是一款定位中高端的通用型SOC&#xff0c;采用22nm制程工艺&#xff0c;搭载一颗四核Cortex-A55处理器和Mali G52 2EE 图形处理器。RK3568 支持4K 解码和 1080P 编码&#xff0c;支持SATA/PCIE/USB3.0 外围接口。RK3568内置独立NPU&#xff0c;可用于轻量级人工…

PHP 基础

PHP 基础 概述 在PHP 文件中&#xff0c;可以与HTML 和JavaScript 混编。 开始标记<?php 表示进入PHP 模式&#xff0c;结束标记?>&#xff0c;标识退出PHP 模式。 PHP 模式之外的内容会被作为字符输出到浏览器中。 PHP 在服务端执行&#xff0c;HTML 和 JS 在浏览…

任何人不知道这款超实用的配音软件,我都会伤心的OK?

看完一段精彩的视频&#xff0c;令人陶醉的原因之一就是配音&#xff0c;有的充满感情&#xff0c;有的字正腔圆&#xff0c;相信很多人都不知道这样的声音是怎么配出来的&#xff1f;今天&#xff0c;小编就来给大家分享一款超实用的配音软件&#xff0c;不仅操作简单&#xf…

STM32:TIM通道输入捕获

本文主要讲解如何使用TIMER通道的输入脉冲捕获功能。基于STM32F7的Timer2 Channel3来进行讲解。 配置时钟 Timer2的时钟频率&#xff0c;对应APB1 Timer。 分频设置为96-1&#xff0c;这样设置每次count计数&#xff0c;对应的时间为1us。Counter设置为最大即可&#xff0c;默…

基于Java的师生交流答疑管理系统设计与实现(源码+lw+部署文档+讲解等)

文章目录 前言具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序&#xff08;小蔡coding&#xff09; 代码参考数据库参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作者&am…

Linux1024一篇通俗易懂的liunx命令操作总结(第十课)

Linux1024一篇通俗易懂的liunx命令操作总结(第十课) 一 liunx 介绍 Linux是一种免费开源的操作系统&#xff0c;它的设计基于Unix。它最早是由芬兰的一位大学生Linus Torvalds在1991年开始编写的&#xff0c;取名为Linux。Linux具有高度的灵活性和可定制性&#xff0c;可以在…

nginx负载均衡(动静分离)

nginx负载均衡&#xff08;动静分离&#xff09; 文章目录 nginx负载均衡&#xff08;动静分离&#xff09;工作原理&#xff1a;环境说明&#xff1a;部署nginx负载均衡步骤&#xff1a;在负载均衡&#xff08;NGINX&#xff09;主机上做配置&#xff1a;测试&#xff1a;在浏…

疯狂小杨哥有意退网

我是卢松松&#xff0c;点点上面的头像&#xff0c;欢迎关注我哦&#xff01; 昨天我发了一个文章说&#xff1a;有1.6亿粉丝的疯狂小杨哥也似乎宣布退网&#xff0c;有些人说我胡编乱造&#xff0c;有些人说我为了博眼球什么都敢做。毕竟做了近10多年自媒体了&#xff0c;我不…

Nginx Proxy代理

代理原理 反向代理产生的背景&#xff1a; 在计算机世界里&#xff0c;由于单个服务器的处理客户端&#xff08;用户&#xff09;请求能力有一个极限&#xff0c;当用户的接入请求蜂拥而入时&#xff0c;会造成服务器忙不过来的局面&#xff0c;可以使用多个服务器来共同分担成…

vue真实项目还原

目录 前言一&#xff0c;初步了解&#xff0c;确定方向二&#xff0c;还原数据库三&#xff0c;启动api网站四&#xff0c;启动vue的前台和后台1、vue2的版本依赖踩坑&#xff08;client_admin&#xff09;2、node-sass安装踩坑&#xff08;client_home&#xff09;&#xff08…

2023年中国印刷电路板行业研究报告

第一章 行业概况 1.1 行业简介 印刷电路板&#xff08;Printed Circuit Board, PCB&#xff09;是电子工业的基石&#xff0c;被誉为“电子产品之母”。它在电子设备中扮演着至关重要的角色&#xff0c;为电路中的各类元器件提供了机械支撑&#xff0c;并确保了电子组件之间的…

驱动实现LED点灯

demo.c #include <linux/init.h> #include <linux/module.h> #include <linux/fs.h> #include <linux/uaccess.h> #include <linux/io.h> #include "head.h" //定义三个指针指向映射后的虚拟内存 unsigned int *vir_moder; unsigned …