【Redis】简单动态字符串SDS

news2024/11/27 22:42:15

C语言字符串

char *str = "redis"; // 可以不显式的添加\0,由编译器添加
char *str = "redis\0"; // 也可以添加\0代表字符串结束

C语言中使用char*字符数组表示字符串,‘\0’来标记一个字符串的结束,不过在使用的过程中我们不需要显式的在字符串中加入’\0’。

存在问题

1.二进制安全

C语言以’\0’标记字符串的结尾,如果一个字符串本身带有’\0’,比如一些二进制数据,那么字符串就会被截断,导致无法存储二进制数据。

2.缓冲区溢出

假设内存有两个相邻的字符串s1和s2,s1保存了字符串“Redis”,s2中保存了字符串“MongoDB”,如果不对大小进行判断,直接调用strcat(s1, “Cluster”)函数在s1字符串后面追加“Cluster“,s1的数据溢出到s2所在空间,导致s2的内容被意外的修改。

3.频繁的内存分配

因为C字符串不记录自身长度,如果对字符串进行修改,需要内存重分配扩展底层数组的空间大小,比如调用strcat函数进行拼接时,需要重新分配内存,保证数组有足够的空间,否则就会发生缓冲区溢出。

由于内存重分配涉及复杂算法,并且可能需要执行系统调用,所以是一个耗时的操作。

4.获取字符串长度复杂度为O(N)

C字符串不记录自身长度,需要遍历每个字符计算字符串长度,在高并发情况下有可能称为性能的瓶颈。

参考:黄健宏《Redis设计与实现》

Redis String

String字符串是Redis中的一种数据结构,它的语法如下:

SET key value

key和value都是字符串,一个key对应一个value,通过key可以获取到对应的value:

GET KEY

简单动态字符串

Redis的字符串没有直接使用C语言的字符数组实现,而是通过SDS(Simple Dynamic String)简单动态字符串实现的。

SDS数据结构
typedef char *sds;

/* Note: sdshdr5 is never used, we just access the flags byte directly.
 * However is here to document the layout of type 5 SDS strings. */
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[];
};

Redis为了灵活的保存不同大小的字符串节省内存空间,设计了不同的结构头sdshdr64、sdshdr32、sdshdr16、sdshdr8和sdshdr5。

虽然结构头不同,但是他们都具有相同的属性(sdshdr5除外):

  • len:字符数组buf实际使用的大小,也就是字符串的长度
  • alloc:字符数组buf分配的空间大小
  • flags:标记SDS的类型:sdshdr64、sdshdr32、sdshdr16、sdshdr8和sdshdr5
  • buf[]:字符数组,用来存储实际的字符数据

一些优化

Redis使用了_attribute_ (( __packed__))节省内存空间,它可以告诉编译器使用紧凑的方式分配内存,不使用字节对齐的方式给变量分配内存。

关于字节对齐可参考结构体字节对齐,C语言结构体字节对齐详解。

柔性数组

在结构体定义中,可以看到最后一个buf数组是没有设置大小的,这种放在结构体中最后一个元素位置并且没有设置大小的数组称为柔性数组,它可以在程序运行过程中动态的进行内存分配。

SDS创建

(1)sds在创建的时候,buf数组初始大小为:struct结构体大小 + 字符串的长度+1, +1是为了在字符串末尾添加一个\0。

(2)在完成字符串到字符数组的拷贝之后,会在字符串末尾加一个\0,这样可以复用C语言的一些函数。

sds _sdsnewlen(const void *init, size_t initlen, int trymalloc) {
    void *sh;
    sds s;
    // 根据长度计算sds类型
    char type = sdsReqType(initlen);
    if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
    // 获取结构体大小
    int hdrlen = sdsHdrSize(type);
    unsigned char *fp; /* flags pointer. */
    size_t usable;

    assert(initlen + hdrlen + 1 > initlen); /* Catch size_t overflow */
    // 分配内存空间,初始大小为:struct结构体大小+字符串的长度+1,+1是为了在字符串末尾添加一个\0
    sh = trymalloc?
        s_trymalloc_usable(hdrlen+initlen+1, &usable) :
        s_malloc_usable(hdrlen+initlen+1, &usable);
    // 如果分配失败
    if (sh == NULL) return NULL;
    if (init==SDS_NOINIT)
        init = NULL;
    else if (!init)
        memset(sh, 0, hdrlen+initlen+1);
    // 指向buf数组的指针
    s = (char*)sh+hdrlen;
    fp = ((unsigned char*)s)-1;
    usable = usable-hdrlen-1;
    if (usable > sdsTypeMaxSize(type))
        usable = sdsTypeMaxSize(type);
    // 类型选择
    switch(type) {
        case SDS_TYPE_5: {
            *fp = type | (initlen << SDS_TYPE_BITS);
            break;
        }
        case SDS_TYPE_8: {
            SDS_HDR_VAR(8,s);
            sh->len = initlen;
            sh->alloc = usable;
            *fp = type;
            break;
        }
        case SDS_TYPE_16: {
            SDS_HDR_VAR(16,s);
            sh->len = initlen;
            sh->alloc = usable;
            *fp = type;
            break;
        }
        case SDS_TYPE_32: {
            SDS_HDR_VAR(32,s);
            sh->len = initlen;
            sh->alloc = usable;
            *fp = type;
            break;
        }
        case SDS_TYPE_64: {
            SDS_HDR_VAR(64,s);
            sh->len = initlen; // 设置字符串长度
            sh->alloc = usable; // 设置分配的总空间大小
            *fp = type; // 设置sds类型
            break;
        }
    }
    if (initlen && init)
        memcpy(s, init, initlen); // 将字符串拷贝到buf数组
    // 字符串末尾添加一个\0
    s[initlen] = '\0';
    return s;
}

// 获取结构体大小
static inline int sdsHdrSize(char type) {
    switch(type&SDS_TYPE_MASK) {
        case SDS_TYPE_5:
            return sizeof(struct sdshdr5);
        case SDS_TYPE_8:
            return sizeof(struct sdshdr8);
        case SDS_TYPE_16:
            return sizeof(struct sdshdr16);
        case SDS_TYPE_32:
            return sizeof(struct sdshdr32);
        case SDS_TYPE_64:
            return sizeof(struct sdshdr64);
    }
    return 0;
}
SDS扩容

(1)判断剩余可用空间是否大于需要增加的长度,如果大于说明空间足够,直接返回即可,反之计算新的长度,进行扩容;

(2)空间预分配,扩容新的长度为已经使用的长度+需要增加的长度,然后会判断是否小于SDS_MAX_PREALLOC(1M):

  • 如果小于,直接扩容为新长度的2倍
  • 如果大于,扩容容量为新长度+SDS_MAX_PREALLOC的值

(3)由于扩容之后大小发生了改变,需要重新计算使用哪种SDS类型:

  • 如果类型不需要改变,直接扩容即可
  • 如果类型发生改变,需要重新进行内存分配,并将旧的内存释放
sds sdsMakeRoomFor(sds s, size_t addlen) {
    void *sh, *newsh;
    // 获取剩余可用空间大小
    size_t avail = sdsavail(s);
    size_t len, newlen;
    char type, oldtype = s[-1] & SDS_TYPE_MASK;
    int hdrlen;
    size_t usable;

    // 如果可用空间大于需要增加的长度,返回即可
    if (avail >= addlen) return s;
    // 获取实际使用的长度
    len = sdslen(s);
    sh = (char*)s-sdsHdrSize(oldtype);
    // 计算新的长度,已经使用的长度+需要增加的长度
    newlen = (len+addlen);
    assert(newlen > len);   /* Catch size_t overflow */
    // 如果新的长度小于SDS_MAX_PREALLOC
    if (newlen < SDS_MAX_PREALLOC)
        // 扩容为新长度的2倍
        newlen *= 2;
    else
        // 新长度 = 新长度+SDS_MAX_PREALLOC
        newlen += SDS_MAX_PREALLOC;
    // 根据新的长度计算需要使用sds的类型
    type = sdsReqType(newlen);
    if (type == SDS_TYPE_5) type = SDS_TYPE_8;
    // 获取struct大小
    hdrlen = sdsHdrSize(type);
    assert(hdrlen + newlen + 1 > len);  /* Catch size_t overflow */
    // 如果sds类型不需要改变
    if (oldtype==type) {
        // 扩容
        newsh = s_realloc_usable(sh, hdrlen+newlen+1, &usable);
        if (newsh == NULL) return NULL;
        s = (char*)newsh+hdrlen;
    } else {
        // 如果需要改变sds类型,重新分配空间
        newsh = s_malloc_usable(hdrlen+newlen+1, &usable);
        if (newsh == NULL) return NULL;
        // 拷贝字符串到字符数组
        memcpy((char*)newsh+hdrlen, s, len+1);
        // 释放旧的空间
        s_free(sh);
        s = (char*)newsh+hdrlen;
        s[-1] = type;
        sdssetlen(s, len);
    }
    usable = usable-hdrlen-1;
    if (usable > sdsTypeMaxSize(type))
        usable = sdsTypeMaxSize(type);
    sdssetalloc(s, usable);
    return s;
}

惰性空间释放

当字符串缩短后,程序并不会立刻释放多余的空间,之后可直接复用多余的空间。

Redis SDS总结

  1. 直接保存了字符串的长度,不需要遍历每个字符去计算长度。
  2. 使用了字符数组保存数据,并且记录了字符串长度,通过长度来判断字符串的结束而不是\0,可以保存二进制数据。
  3. 需要改变字符串长度大小时,会通过sdsMakeRoomFor方法确保有足够的内存空间,不需要开发人员自行判断,保证了数据的安全性,不会造成缓冲区溢出。
  4. 在进行扩容时,会进行空间预分配,多分配一些空间,减小内存分配的次数。
  5. 创建了不同的结构体,保存不同大小的字符串节省内存空间。
  6. 使用_attribute_ (( __packed__))紧凑的方式分配内存,节省内存空间。

参考

极客时间 - Redis源码剖析与实战(蒋德钧)

yellowriver007 - Redis内部数据结构详解(2)——sds

偷懒的程序员-小彭 - redis 系列,要懂redis,首先得看懂sds(全网最细节的sds讲解)

CHENG Jian - C语言0长度数组(可变数组/柔性数组)详解

Redis版本:redis-6.2.5

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

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

相关文章

CAA DMU模块仿真

背景 本人由于项目原因&#xff0c;需要基于CATIA格式文件研究CAM的一些操作&#xff0c;其中就包括仿真功能&#xff0c;而CATIA中适合实现仿真功能的模块就是 DMU (Digital Mock-Up) 模块&#xff0c;本人研究了很长时间&#xff0c;尝试了很多方案&#xff0c;特地记录下来…

华为云云耀云服务器L实例评测|Docker部署及应用

文章目录 前言&#x1f4e3; 1.前言概述&#x1f4e3; 2.服务器攻击✨ 2.1 问题描述✨ 2.2 处理方法 &#x1f4e3; 3.Docker简介&#x1f4e3; 4.安装Docker✨ 4.1 卸载旧版docker✨ 4.2 安装依赖包✨ 4.3 安装GPG证书✨ 4.4 配置仓库✨ 4.5 正式安装Docker✨ 4.6 配置用户组✨…

Spring Boot的魔法:构建高性能Java应用

文章目录 Spring Boot&#xff1a;简化Java开发Spring Boot的性能优势1. 内嵌服务器2. 自动配置3. 起步依赖4. 缓存和优化5. 异步处理 实际示例&#xff1a;构建高性能的RESTful API总结 &#x1f389;欢迎来到架构设计专栏~Spring Boot的魔法&#xff1a;构建高性能Java应用 ☆…

洛谷P1102 A-B 数对题解

目录 题目A-B 数对题目背景题目描述输入格式输出格式样例 #1样例输入 #1样例输出 #1提示传送门 代码解释亲测 题目 A-B 数对 题目背景 出题是一件痛苦的事情&#xff01; 相同的题目看多了也会有审美疲劳&#xff0c;于是我舍弃了大家所熟悉的 AB Problem&#xff0c;改用 …

Python与Scrapy:构建强大的网络爬虫

网络爬虫是一种用于自动化获取互联网信息的工具&#xff0c;在数据采集和处理方面具有重要的作用。Python语言和Scrapy框架是构建强大网络爬虫的理想选择。本文将分享使用Python和Scrapy构建强大的网络爬虫的方法和技巧&#xff0c;帮助您快速入门并实现实际操作价值。 一、Pyt…

“链圈”十年反思

2013 年 11 月&#xff0c;Vitalik Buterin 发表了以太坊白皮书的第一个版本。事后人们经常把这视为“区块链 2.0” 时代开启的标志&#xff0c;但在当时&#xff0c;其实是以太坊的出现才使得“区块链”作为一项单独的技术从“数字货币”当中分离出来。换句话说&#xff0c;比…

基于Java的大学生英语考试答题系统设计与实现(亮点:答题系统、报名系统、在线视频、在线聊天、附件下载)

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

2023 年最佳多 GPU 深度学习系统指南

动动发财的小手&#xff0c;点个赞吧&#xff01; 本文[1]提供了有关如何构建用于深度学习的多 GPU 系统的指南&#xff0c;并希望为您节省一些研究时间和实验时间。 1. GPU 让我们从有趣&#xff08;且昂贵&#xff09;的部分开始&#xff01; 购买 GPU 时的主要考虑因素是&am…

flutter开发实战-webview插件flutter_inappwebview使用

flutter开发实战-webview插件flutter_inappwebview使用 在开发过程中&#xff0c;经常遇到需要使用WebView&#xff0c;Webview需要调用原生的插件来实现。常见的flutter的webview插件是webview_flutter&#xff0c;flutter_inappwebview。之前整理了一下webview_flutter&…

htb-cozyhosting

HTB-CozyHosting https://app.hackthebox.com/machines/CozyHosting ──(kwkl㉿kwkl)-[~] └─$ tail -l /etc/hosts …

凉鞋的 Unity 笔记 103. 检视器:GameObject 的微观编辑和查看

103. 检视器&#xff1a;GameObject 的微观编辑和查看 在上一篇&#xff0c;笔者简单介绍了场景层次 与 GameObject 的增删改查&#xff0c;如下所示&#xff1a; 在这一篇&#xff0c;我们接着往下学习。 我们知道在 场景层次 窗口&#xff0c;可以对 GameObject 进行增删改…

金融帝国实验室(CapLab)官方更新_V9.1.15版本(2023年第64次)

〖金融帝国实验室〗&#xff08;Capitalism Lab&#xff09;游戏更新记录&#xff08;2023年度&#xff09; ————————————— ◎游戏开发&#xff1a;Enlight Software Ltd.&#xff08;微启软件有限公司&#xff09; ◎官方网站&#xff1a;https://www.capitalis…

力扣-338.比特位计数

Idea 直接暴力做法&#xff1a;计算从0到n&#xff0c;每一位数的二进制中1的个数&#xff0c;遍历其二进制的每一位即可得到1的个数 AC Code class Solution { public:vector<int> countBits(int n) {vector<int> ans;ans.emplace_back(0);for(int i 1; i < …

洛谷P5732 【深基5.习7】杨辉三角题解

目录 题目【深基5.习7】杨辉三角题目描述输入格式输出格式样例 #1样例输入 #1样例输出 #1传送门 代码解释亲测 题目 【深基5.习7】杨辉三角 题目描述 给出 n ( n ≤ 20 ) n(n\le20) n(n≤20)&#xff0c;输出杨辉三角的前 n n n 行。 如果你不知道什么是杨辉三角&#xf…

基于SpringBoot的每日推购物推荐网站的设计与实现

目录 前言 一、技术栈 二、系统功能介绍 商品信息管理 销售排行统计 商品类型管理 个人信息 商品 我的订单管理 三、核心代码 1、登录模块 2、文件上传模块 3、代码封装 前言 随着信息互联网购物的飞速发展&#xff0c;一般企业都去创建属于自己的电商平台以及购物管…

Emacs之default-tab-width与tab-width用法总结(一百二十九)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 人生格言&#xff1a; 人生…

IO流之File类

File类 File 对应的硬盘上的文件或者文件夹 位于java.io包下 File对文件/文件夹进行操作&#xff0c;但是无法对文件内容进行操作&#xff0c;读取/写入不可以操作&#xff0c;但是可以创文件夹/读取文件路径,IO流才可以进行操作 文件/文件夹的路径&#xff1a;linux使用/作为文…

【剑指Offer】8.二叉树的下一个结点

题目 给定一个二叉树其中的一个结点&#xff0c;请找出中序遍历顺序的下一个结点并且返回。注意&#xff0c;树中的结点不仅包含左右子结点&#xff0c;同时包含指向父结点的next指针。下图为一棵有9个节点的二叉树。树中从父节点指向子节点的指针用实线表示&#xff0c;从子节…

SSM 中的拦截器(Interceptor):作用与实现原理

SSM 中的拦截器&#xff08;Interceptor&#xff09;&#xff1a;作用与实现原理 拦截器&#xff08;Interceptor&#xff09;是 Spring 框架中的一个重要组件&#xff0c;也在 Spring Spring MVC MyBatis&#xff08;SSM&#xff09;等框架中起到了关键作用。本文将深入探讨…

阿里云关系型数据库RDS详细说明

阿里云RDS关系型数据库大全&#xff0c;关系型数据库包括MySQL版、PolarDB、PostgreSQL、SQL Server和MariaDB等&#xff0c;NoSQL数据库如Redis、Tair、Lindorm和MongoDB&#xff0c;阿里云百科分享阿里云RDS关系型数据库大全&#xff1a; 目录 阿里云RDS关系型数据库大全 …