Redis原理再学习:动态字符串sds

字符

字符就是英文里的一个一个英文字母,比如:a。中文里的单个汉字,比如:好。

字符串就是多个字母或多个汉字组成,比如字符串:redis,中文字符串:你好吗。

英文字符,如果按照 ASCII 码计算,一个字符占用 1 个字节。

中文字符的编码就比较复杂点,一个字符占用空间一般是 2 个字节,有的也用 3-4 个字节。

它有很多格式编码,有 gb2312,gbk, utf8 等等。

具体可以看看这篇文章:常见的中文字符编码

动态字符串sds定义

看看 redis3.0 中的字符数据结构定义,sds.h/sdshdr

// https://github.com/redis/redis/blob/3.0/src/sds.h#L41
// redis3.0 低版本容易理解 // sds 兼容 C 语言风格字符串,并且还可以存到 sdshdr 结构的 buf 里
typedef char *sds; struct sdshdr {
unsigned int len;
unsigned int free;
char buf[];
};
  • len:记录 buf 数组中字符的长度

  • free:记录 buf 中未使用的字节的数量

  • buf:字节数组,保存字符串

不过到了 redis3.2 后,进一步优化了 SDS 的数据结构:

// https://github.com/redis/redis/blob/3.2/src/sds.h#L42
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 { // 对应的字符串长度小于 1<<5
unsigned char flags;/* 3 lsb of type, and 5 msb of string length */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 { // 对应的字符串长度小于 1<<8
uint8_t len; // 已使用长度,1 字节/* 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 { // 对应的字符串长度小于 1<<16
uint16_t len; // 已使用长度,2 字节
uint16_t alloc;
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 { // 对应的字符串长度小于 1<<32
uint32_t len; // 已使用长度,4 字节
uint32_t alloc;
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 { // 对应的字符串长度小于 1<<64
uint64_t len; // 已使用长度,8 字节
uint64_t alloc;
unsigned char flags;
char buf[];
};

针对不同长度的字符串,申请相应的数据存储类型,从而有效的节约了内存使用,进一步优化存储。进一步压榨 redis 的性能。

sdshdr为什么要这样设计

C 语言中的字符数组难道不够用吗?(redis3.0)

对于普通的字符串操作,C 语言中的字符数组是够用的。但是,redis 定位是高性能 kv 数据库,所以对于 redis 是不够用的。

从哪些方面优化才能扣出一点点性能呢?

  1. 空间换时间:一次多分配一些空间,下次增加字符串时不需要在进行分配空间的操作了。这个叫预分配。

    ​ 还有,截断字符串时,也不需要归还空间,而是用 free 属性记录,下次可以在复用空间,这个叫惰性空间释放。

  2. 获取字符串长度复杂度 O(1):sdshdr 里面定义了一个属性 len,操作字符串时会自动计算字符串的长度并赋值给这个 len 属性。所以在 redis 的一些命令中,不需要在计算一次字符串长度,而计算这个长度复杂度是 O(N)。比如 strlen 命令。

除了上面的 2 点,还有哪些好处?

  1. 避免缓存区溢出:因为 SDS 的空间分配策略杜绝了发生缓冲区溢出的可能性。因为 SDS 的 API 会先检查空间是否满足修改所需的要求,不满足的话会自动扩展修改所需的空间大小。
  2. 二进制安全:得益于 sds api 的设计,所有 sds api 都会以处理二进制的方式处理 sds 存放在 buf 数组里的数据,判断字符串是否到达结尾,不是以C语言里的 \0 来判断,而是用 sdshdr 结构里的 len 属性。这就避免了字符串中间遇到 \0 而发生错误判断。

SDS内存怎么分配

其实上面我们也有提到,一种是预分配,一种是惰性分配(惰性释放)。

1. 预分配

空间预分配用于优化sds字符串增长的操作:

  • 如果对 sds 进行修改时,sds 的 len < 1MB,那么会分配和 len 长度相同的未使用空间,未使用空间长度记录到 free。
  • 如果对 sds 进行修改时,sds 的 len > 1MB,那么分配 1MB 的未使用空间,记录到 free。

通过这种预分配策略,减少 Redis 执行字符串增长操作时,所需内存重新分配的次数,提高 redis 的效率。

空间预分配代码 sds.c/sdsMakeRoomFor:

// https://github.com/redis/redis/blob/3.0/src/sds.c#L129
/* Enlarge the free space at the end of the sds string so that the caller
* is sure that after calling this function can overwrite up to addlen
* bytes after the end of the string, plus one more byte for nul term.
*
* Note: this does not change the *length* of the sds string as returned
* by sdslen(), but only the free buffer space we have. */
sds sdsMakeRoomFor(sds s, size_t addlen) {
struct sdshdr *sh, *newsh;
size_t free = sdsavail(s); // 获取未使用空间free值
size_t len, newlen; if (free >= addlen) return s; // free 如果够用直接返回
len = sdslen(s);
sh = (void*) (s-(sizeof(struct sdshdr)));
newlen = (len+addlen); // 扩展后的新长度
// #define SDS_MAX_PREALLOC (1024*1024)
if (newlen < SDS_MAX_PREALLOC) // 新长度小于定义的最大预分配长度(1MB),那么把新长度直接扩大2倍
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC;// 新长度大于定义的最大预分配长度(1MB),那么 newlen+1MB
newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1); // 获取新空间的地址
if (newsh == NULL) return NULL; newsh->free = newlen - len; // 更新未使用空间 free
return newsh->buf;
} // https://github.com/redis/redis/blob/3.0/src/sds.h#L52
// 获取 sds 未使用空间长度,free
static inline size_t sdsavail(const sds s) {
struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
return sh->free;
} // https://github.com/redis/redis/blob/3.0/src/sds.h#L47
// 获取 sds 中实际字符串长度,len
static inline size_t sdslen(const sds s) {
struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
return sh->len;
} // https://github.com/redis/redis/blob/3.0/src/sds.h#L34
#define SDS_MAX_PREALLOC (1024*1024)

2.惰性分配(惰性释放)

惰性分配,就是惰性空间释放,用于对优化sds的字符串缩减操作。

  • 如果 sds 保存的字符串缩减时,sds api 并不会直接回收内存,而是用 free 属性成员将不用内存字节大小记录下来,等待将来使用。

这样就优化了对内存的操作。

代码 sds.c/sdsclear:

/* Modify an sds string in-place to make it empty (zero length).
* However all the existing buffer is not discarded but set as free space
* so that next append operations will not require allocations up to the
* number of bytes previously available. */
// https://github.com/redis/redis/blob/3.0/src/sds.c#L112
void sdsclear(sds s) {
struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));
sh->free += sh->len; // 新的free = 新的free + 使用空间len
sh->len = 0; // 已使用空间置为 0
sh->buf[0] = '\0'; // 字符串置为空
}

sdshdr存储字符串示例

比如 sdshdr 存储一个字符串 Redis,如下图:

​ (《Redis设计与实现》)

存储 Redis 字符串时 sdshdr 各属性解释:

  • free:值为 0,表示 sdshdr 中没有未使用的空间

  • len:值为 5,表示 sdshdr 保存了一个 5 字节长度的字符串

  • buf:char 类型的数组,数组前 5 个字节保存了 R, e, d, i, s 五个字符,结尾保存了空字符 '\0'。这个遵循了 c 语言中空字符串结尾惯例。保存这个空字符串的 1 字节长度不计算在 sdshdr 的 len 属性里。

参考

Redis原理再学习02:数据结构-动态字符串sds的更多相关文章

  1. Redis原理再学习04:数据结构-哈希表hash表(dict字典)

    哈希函数简介 哈希函数(hash function),又叫散列函数,哈希算法.散列函数把数据"压缩"成摘要,有的也叫"指纹",它使数据量变小且数据格式大小也固定 ...

  2. Redis原理再学习05:数据结构-整数集合intset

    intset介绍 intset 整数集合,当一个集合只有整数元素,且元素数量不多时,Redis 就会用整数集合作为集合键的底层实现. redis> SADD numbers 1 3 5 7 9 ...

  3. redis源码学习_简单动态字符串

    SDS相比传统C语言的字符串有以下好处: (1)空间预分配和惰性释放,这就可以减少内存重新分配的次数 (2)O(1)的时间复杂度获取字符串的长度 (3)二进制安全 主要总结一下sds.c和sds.h中 ...

  4. Redis源码阅读一:简单动态字符串SDS

    源码阅读基于Redis4.0.9 SDS介绍 redis 127.0.0.1:6379> SET dbname redis OK redis 127.0.0.1:6379> GET dbn ...

  5. redis底层数据结构之简单动态字符串(SDS)

    简单动态字符串(simple dynamic string,SDS) redis使用C语言编写的,但是redis的字符串却不是C语言中的字符串(以空字符'\0'结尾的字符数组),redis定义了一种简 ...

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

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

  7. redis 5.0.7 源码阅读——动态字符串sds

    redis中动态字符串sds相关的文件为:sds.h与sds.c 一.数据结构 redis中定义了自己的数据类型"sds",用于描述 char*,与一些数据结构 typedef c ...

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

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

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

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

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

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

随机推荐

  1. [转帖]Optimizing Block Device Parameter Settings of Linux

    https://support.huawei.com/enterprise/en/doc/EDOC1000181485/ddbc0e8b/optimizing-block-device-paramet ...

  2. 【转帖】Dockerfile文件指令介绍

    https://blog.whsir.com/post-5327.html Dockerfile其实就是一个文本文件,这个文本文件名称叫Dockerfile,里面包含了一些指令(可以理解成多个指令集合 ...

  3. [转帖]shell脚本中$0 $1 $# $@ $* $? $ 的各种符号的意义

    概述 shell中有两类字符,一类是普通字符,在Shell中除了本身的字面意思外没有其他特殊意义,即普通纯文本:另一类即元字符,是Shell的保留字符,在Shell中有着特殊的含义. 今天主要介绍一下 ...

  4. [转帖]Linux如何查看网关地址

      转至:https://baijiahao.baidu.com/s?id=1733537078943023051&wfr=spider&for=pc 服务器之间的通信是通过ip地址来 ...

  5. [转帖]SYSTEMD 配置文件

    https://www.cnblogs.com/xiexun/p/13643952.html [Unit]区块通常是配置文件的第一个区块,用来定义 Unit 的元数据,以及配置与其他 Unit 的关系 ...

  6. Nacos集群启动注意事项

    简介 Nacos是阿里巴巴开源的一套服务注册发现的应用 使用简单灵活, 是spring Cloud Alibaba的组成部分 现在拆分微服务的部署情况下,极大的需求nacos服务作为支撑 单点情况下存 ...

  7. SQLSERVER 标准版与企业版的版本标识区别

    1.  windows 标准版  sqlserver 标准版 2. Windows 数据中心版 sqlserver 企业版 3. Win10 之后 服务器版本缩减的很厉害 只有两个版本了 如图示 4. ...

  8. 学到一个编码技巧:用重复写入代替if判断,减少程序分支

    作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢! cnblogs博客 zhihu Github 公众号:一本正经的瞎扯 近期阅读了rust标准库的hashbrown库(也就是一个 ...

  9. 2023中国高校计算机大数据挑战赛:论文学科分类baseline|清华主办

    2023中国高校计算机大数据挑战赛:论文学科分类baseline|清华主办 官方地址:https://www.heywhale.com/home/competition 项目码源见文末 1.比赛介绍 ...

  10. 爆了!Sealos 三天支持 1000 个帕鲁私服

    Sealos 的帕鲁私服模板从第一天发布之后就起了 100 多个私服,第二天直接上到 500 多个,第三天直接上千,还在加速增长中.来讲讲我们只用一个晚上怎么做到上线一个专属可用区的,还有一些帕鲁实践 ...