Redis原理再学习02:数据结构-动态字符串sds
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 是不够用的。
从哪些方面优化才能扣出一点点性能呢?
空间换时间:一次多分配一些空间,下次增加字符串时不需要在进行分配空间的操作了。这个叫预分配。
还有,截断字符串时,也不需要归还空间,而是用 free 属性记录,下次可以在复用空间,这个叫惰性空间释放。
获取字符串长度复杂度 O(1):sdshdr 里面定义了一个属性 len,操作字符串时会自动计算字符串的长度并赋值给这个 len 属性。所以在 redis 的一些命令中,不需要在计算一次字符串长度,而计算这个长度复杂度是 O(N)。比如 strlen 命令。
除了上面的 2 点,还有哪些好处?
- 避免缓存区溢出:因为 SDS 的空间分配策略杜绝了发生缓冲区溢出的可能性。因为 SDS 的 API 会先检查空间是否满足修改所需的要求,不满足的话会自动扩展修改所需的空间大小。
- 二进制安全:得益于 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设计与实现》 作者:黄健宏
- redis.io
- github redis
Redis原理再学习02:数据结构-动态字符串sds的更多相关文章
- Redis原理再学习04:数据结构-哈希表hash表(dict字典)
哈希函数简介 哈希函数(hash function),又叫散列函数,哈希算法.散列函数把数据"压缩"成摘要,有的也叫"指纹",它使数据量变小且数据格式大小也固定 ...
- Redis原理再学习05:数据结构-整数集合intset
intset介绍 intset 整数集合,当一个集合只有整数元素,且元素数量不多时,Redis 就会用整数集合作为集合键的底层实现. redis> SADD numbers 1 3 5 7 9 ...
- redis源码学习_简单动态字符串
SDS相比传统C语言的字符串有以下好处: (1)空间预分配和惰性释放,这就可以减少内存重新分配的次数 (2)O(1)的时间复杂度获取字符串的长度 (3)二进制安全 主要总结一下sds.c和sds.h中 ...
- Redis源码阅读一:简单动态字符串SDS
源码阅读基于Redis4.0.9 SDS介绍 redis 127.0.0.1:6379> SET dbname redis OK redis 127.0.0.1:6379> GET dbn ...
- redis底层数据结构之简单动态字符串(SDS)
简单动态字符串(simple dynamic string,SDS) redis使用C语言编写的,但是redis的字符串却不是C语言中的字符串(以空字符'\0'结尾的字符数组),redis定义了一种简 ...
- 图解Redis之数据结构篇——简单动态字符串SDS
图解Redis之数据结构篇--简单动态字符串SDS 前言 相信用过Redis的人都知道,Redis提供了一个逻辑上的对象系统构建了一个键值对数据库以供客户端用户使用.这个对象系统包括字符串对象 ...
- redis 5.0.7 源码阅读——动态字符串sds
redis中动态字符串sds相关的文件为:sds.h与sds.c 一.数据结构 redis中定义了自己的数据类型"sds",用于描述 char*,与一些数据结构 typedef c ...
- Redis底层探秘(一):简单动态字符串(SDS)
redis是我们使用非常多的一种缓存技术,他的性能极高,读的速度是110000次/s,写的速度是81000次/s.这么高的性能背后,到底是怎么样的实现在支撑,这个系列的文章,我们一起去看看. redi ...
- Redis数据结构之简单动态字符串SDS
Redis的底层数据结构非常多,其中包括SDS.ZipList.SkipList.LinkedList.HashTable.Intset等.如果你对Redis的理解还只停留在get.set的水平的话, ...
- redis 系列3 数据结构之简单动态字符串 SDS
一. SDS概述 Redis 没有直接使用C语言传统的字符串表示,而是自己构建了一种名为简单动态字符串(simple dynamic string, SDS)的抽象类型,并将SDS用作Redis的默 ...
随机推荐
- [转帖]金仓数据库KingbaseES分区表 -- 声明式创建分区表
https://www.modb.pro/db/638045 1. 创建分区表同时创建分区 1.1 准备环境 # 创建分区表同时创建分区 create table tb1(id bigint,stat ...
- 【转帖】71.常用的显示GC日志的参数、GC日志分析、日志分析工具的使用
目录 1.常用的显示GC日志的参数 2.图解垃圾`GC`日志(重要) 3.日志分析工具的使用 1.常用的显示GC日志的参数 解释: 日志中,GC和Full GC表示的是GC的类型.GC只在新生代进行, ...
- Redis性能问题诊断以及scan命令耗时分析
Redis性能问题诊断以及scan命令耗时分析 摘要 最近公司有项目反馈卡顿. 卡顿一小时后自己被拉入群聊. 同事已经基本上定位到问题原因. 我这边想使用朴素的性能观点进行一下性能问题的拆解 为了提高 ...
- Linux查看登录用户记录信息
Linux查看登录用户记录信息 登录成功的信息 last 可以简单统计一下: last |awk '{print $3}' |sort |uniq -c |sort -k1nr 登录失败的 就是 la ...
- 内网CentOS7搭建ntp服务器实现内网时间同步
内网CentOS7搭建ntp服务器实现内网时间同步 背景 公司内部有很多虚拟机,本来很简单的实现了每天晚上自动同步阿里云时间 crontab -e 1 1 * * * ntpdate ntp.aliy ...
- 隐私集合求交(PSI)协议研究综述
摘要 隐私集合求交(PSI)是安全多方计算(MPC)中的一种密码学技术,它允许参与计算的双方,在不获取对方额外信息(除交集外的其它信息)的基础上,计算出双方数据的交集.隐私集合求交在数据共享,广告转化 ...
- elementui表格内容超出显示省略号
有些时候表格的内容太长了: 但是elementui中的表格,会进行换行处理: 此时表格的高度就会发生变化 这样就不好看,此时就要进行省略号来出来这个问题: el-table是有这个控制属性的::sho ...
- Unity Editor开发中查找属性的两种写法对比
从2017开始,在editor脚本中查找属性是这样写的 var m_Script = serializedObject.FindProperty("m_Script"); Seri ...
- vim 从嫌弃到依赖(18)——查找模式进阶
上一篇文章中,我们初步结识了如何使用查找模式,也能够通过n和 N进行查找.这篇将会介绍搜索中更高级的用法.另外在写上一篇文章的时候我发现介绍查找相关内容的时候不能用动图来演示,主要是因为输入的内容太多 ...
- SpringAll
目录 Spring Cloud 01-初识SpringCloud与微服务 02-SpringCloud-Feign声明式服务的调用 Spring Security 01-SpringSecurity- ...