【Redis】简单动态字符串SDS
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总结
- 直接保存了字符串的长度,不需要遍历每个字符去计算长度。
- 使用了字符数组保存数据,并且记录了字符串长度,通过长度来判断字符串的结束而不是\0,可以保存二进制数据。
- 需要改变字符串长度大小时,会通过sdsMakeRoomFor方法确保有足够的内存空间,不需要开发人员自行判断,保证了数据的安全性,不会造成缓冲区溢出。
- 在进行扩容时,会进行空间预分配,多分配一些空间,减小内存分配的次数。
- 创建了不同的结构体,保存不同大小的字符串节省内存空间。
- 使用_attribute_ (( __packed__))紧凑的方式分配内存,节省内存空间。
参考
yellowriver007 - Redis内部数据结构详解(2)——sds
偷懒的程序员-小彭 - redis 系列,要懂redis,首先得看懂sds(全网最细节的sds讲解)
CHENG Jian - C语言0长度数组(可变数组/柔性数组)详解
Redis版本:redis-6.2.5
【Redis】简单动态字符串SDS的更多相关文章
- 图解Redis之数据结构篇——简单动态字符串SDS
		图解Redis之数据结构篇--简单动态字符串SDS 前言 相信用过Redis的人都知道,Redis提供了一个逻辑上的对象系统构建了一个键值对数据库以供客户端用户使用.这个对象系统包括字符串对象 ... 
- Redis底层探秘(一):简单动态字符串(SDS)
		redis是我们使用非常多的一种缓存技术,他的性能极高,读的速度是110000次/s,写的速度是81000次/s.这么高的性能背后,到底是怎么样的实现在支撑,这个系列的文章,我们一起去看看. redi ... 
- Redis—简单动态字符串(SDS)
		目录 Redis-简单动态字符串(SDS) SDS的定义 SDS与C字符串的区别 1. 常数复杂度获取字符串长度: 2. 杜绝缓冲区溢出: 3. 减少修改字符串时带来的内存重分配次数 4. 二进制安全 ... 
- Redis数据结构之简单动态字符串SDS
		Redis的底层数据结构非常多,其中包括SDS.ZipList.SkipList.LinkedList.HashTable.Intset等.如果你对Redis的理解还只停留在get.set的水平的话, ... 
- redis 系列3  数据结构之简单动态字符串 SDS
		一. SDS概述 Redis 没有直接使用C语言传统的字符串表示,而是自己构建了一种名为简单动态字符串(simple dynamic string, SDS)的抽象类型,并将SDS用作Redis的默 ... 
- Redis源码解析:01简单动态字符串SDS
		Redis没有直接使用C字符串(以'\0'结尾的字符数组),而是构建了一种名为简单动态字符串( simple dynamic string, SDS)的抽象类型,并将SDS用作Redis的默认字符 ... 
- Redis核心原理-简单动态字符串SDS
		SDS简介 Redis是C语言编写的,但没有使用c语言的字符串结构,而是自己实现了一套简单动态字符串 simple dynamic string 简称SDS,SDS兼容C语言的字符串类型,原理类似Ja ... 
- 深入理解Redis 数据结构—简单动态字符串sds
		Redis是用ANSI C语言编写的,它是一个高性能的key-value数据库,它可以作用在数据库.缓存和消息中间件.其中 Redis 键值对中的键都是 string 类型,而键值对中的值也是有 st ... 
- Redis5设计与源码分析读后感(二)简单动态字符串SDS
		一.引言 学习之前先了解几个概念: SDS定义:简单动态字符串,Redis的基本数据结构之一,用于储存字符串和整型数据. 二进制安全:C语言中用"\0"表示字符串结束,如果字符串本 ... 
随机推荐
- JS正则表达式学习记录
			JS:正则表达式学习记录 <!DOCTYPE html> <html lang="en"> <head> <meta charset=&q ... 
- 前端之HTML标签
			一:HTML简介 1.超文本标记语言(Hypertext Markup Language, HTML)是一种用于创建网页的标记语言. 2.本质上是浏览器可识别的规则,我们按照规则写网页,浏览器根据规则 ... 
- JAVA 进程线程详解
			线程和进程 一.进程 进程是指运行中的程序,比如我们使用QQ,就启动该进程分配内存空间. 进程是程序的一次执行过程,或是正在运行的一个程序.是一个动态的过程:有它自升的产生,存在和消亡的过程 二.线程 ... 
- 整理display:none;和visibility:hidden;和overflow:hidden;的区别
			1.display:none; 这个属性隐藏元素,不占网页任何空间,彻底隐藏,消失 2.visibility:hidden; 占据空间,但是无法点击.隐藏了这个层,看不到,却能摸得着 3.over ... 
- POJ - 1321 A - 棋盘问题
			A - 棋盘问题 http://poj.org/problem?id=1321 思路:不能搞双重循环嵌套,要注意可以跳过某行 代码 #include <cstdio> #include & ... 
- 初学Java时没有理解的一些概念
			背景 之前学Java属于赶鸭子上架,草草学习基础语法便直接做课程作业,许多概念问题仍不清楚,故在此梳理一下,主要参考廖雪峰和互联网资料. Java运行方式与JVM Java是介于编译型语言(C++)和 ... 
- C# 11 对 ref 和 struct 的改进
			前言 C# 11 中即将到来一个可以让重视性能的开发者狂喜的重量级特性,这个特性主要是围绕着一个重要底层性能设施 ref 和 struct 的一系列改进. 但是这部分的改进涉及的内容较多,不一定能在 ... 
- SQL语言详解
			SQL 1. 概述 Structured Query Language 结构化查询语言 结构化查询语言(Structured Query Language)简称SQL,是一种数据库查询和程序设计语言, ... 
- XCTF练习题---WEB---Training-WWW-Robots
			XCTF练习题---WEB---Training-WWW-Robots flag:cyberpeace{e37180e3f5ad17b4ac71a131e2de1fcb} 解题步骤: 1.观察题目,打 ... 
- 【链表】【leetCode高频】:  19. 删除链表的倒数第 N 个结点
			1.题目描述 给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点. 2.算法分析 知识补充: . 分析: 题目要求是删除链表中倒数第N个结点.可以使用两个指针slow,fast. 重点是 ... 
