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

【Redis】简单动态字符串SDS的更多相关文章

  1. 图解Redis之数据结构篇——简单动态字符串SDS

    图解Redis之数据结构篇--简单动态字符串SDS 前言     相信用过Redis的人都知道,Redis提供了一个逻辑上的对象系统构建了一个键值对数据库以供客户端用户使用.这个对象系统包括字符串对象 ...

  2. Redis底层探秘(一):简单动态字符串(SDS)

    redis是我们使用非常多的一种缓存技术,他的性能极高,读的速度是110000次/s,写的速度是81000次/s.这么高的性能背后,到底是怎么样的实现在支撑,这个系列的文章,我们一起去看看. redi ...

  3. Redis—简单动态字符串(SDS)

    目录 Redis-简单动态字符串(SDS) SDS的定义 SDS与C字符串的区别 1. 常数复杂度获取字符串长度: 2. 杜绝缓冲区溢出: 3. 减少修改字符串时带来的内存重分配次数 4. 二进制安全 ...

  4. Redis数据结构之简单动态字符串SDS

    Redis的底层数据结构非常多,其中包括SDS.ZipList.SkipList.LinkedList.HashTable.Intset等.如果你对Redis的理解还只停留在get.set的水平的话, ...

  5. redis 系列3 数据结构之简单动态字符串 SDS

    一.  SDS概述 Redis 没有直接使用C语言传统的字符串表示,而是自己构建了一种名为简单动态字符串(simple dynamic string, SDS)的抽象类型,并将SDS用作Redis的默 ...

  6. Redis源码解析:01简单动态字符串SDS

    Redis没有直接使用C字符串(以'\0'结尾的字符数组),而是构建了一种名为简单动态字符串( simple  dynamic  string, SDS)的抽象类型,并将SDS用作Redis的默认字符 ...

  7. Redis核心原理-简单动态字符串SDS

    SDS简介 Redis是C语言编写的,但没有使用c语言的字符串结构,而是自己实现了一套简单动态字符串 simple dynamic string 简称SDS,SDS兼容C语言的字符串类型,原理类似Ja ...

  8. 深入理解Redis 数据结构—简单动态字符串sds

    Redis是用ANSI C语言编写的,它是一个高性能的key-value数据库,它可以作用在数据库.缓存和消息中间件.其中 Redis 键值对中的键都是 string 类型,而键值对中的值也是有 st ...

  9. Redis5设计与源码分析读后感(二)简单动态字符串SDS

    一.引言 学习之前先了解几个概念: SDS定义:简单动态字符串,Redis的基本数据结构之一,用于储存字符串和整型数据. 二进制安全:C语言中用"\0"表示字符串结束,如果字符串本 ...

随机推荐

  1. Bugku的exec执行绕过

    题目 思路 1. 打开网页显示403 2. 回去看题目有提示 3. 不用多说,网页访问. 4. 第一行说要传个参数ip,试一下get传参?ip=127.0.0.1 5. 试下 ① 算术运算符 & ...

  2. Java中有关clone方法的用法

    一.clone在数组基本数据类型中的使用 public class Main { public static void main(String[] args) { int[] arr= {7,8,9} ...

  3. Spring MVC 工作原理和流程、注解

    Spring MVC 是实现MVC设计模式的企业级开发框架,是Spring框架的一个子模块,无需整合,开发起来更加便捷. MVC设计模式 MVC是一种设计模式,它将应用程序分为 Controller. ...

  4. git设置本地与远程分支关联

    1.为了避免每次都进行验证,在git进行绑定ssh mkdir ~/.ssh ssh-keygen -t rsa -C "xxx@qq.com" 将生成的公钥粘贴到git中 2.本 ...

  5. 关于C++类定义中不能声明该类对象,而Java中可以的原因

    相信接触过C++的人,在学习Java的过程当中,会遇到这样一个问题:在Java中常常会在类定义中声明一个该类的对象(例如Person类定义中声明一些叫parents之类的Person对象),但是在C+ ...

  6. Percona停服俄罗斯

    2022年3月9日,MySQL重要分支Percona宣布,他们将停止与俄罗斯和白俄罗斯的组织开展新业务,直至另行通知. Percona为支持员工而采取的一些行动如下: 已经在乌克兰目前安全的部分获得了 ...

  7. XCTF练习题---MISC---Get-the-key.txt

    XCTF练习题---MISC---Get-the-key.txt flag:SECCON{@]NL7n+-s75FrET]vU=7Z} 解题步骤: 1.观察题目,下载附件 2.拿到手以后直接惊呆,挺大 ...

  8. ubuntu 18及以上版本配置IP的方法,你get了吗

    本文讲讲 Ubuntu 18 及以上版本配置 IP 的方法,为什么它值得一讲,因为以 Ubuntu 16 为首的版本的配置方法已经不适用了,如果你还不知道,那本文正好 get 一个新技能. Ubunt ...

  9. Linux 查询 OS、CPU、内存、硬盘信息

    点击上方"开源Linux",选择"设为星标" 回复"学习"获取独家整理的学习资料! 一.前言 当我们接手了一台或者几台服务器的时候,首先我们 ...

  10. js动态生成vue组件

    代码奉上 install (Vue, options) { Vue.prototype.$message = function (message){ let Constructor = Vue.ext ...