Redis之简单动态字符串sds

转载:https://segmentfault.com/a/1190000012262739

redis在处理字符串的时候没有直接使用以'\0'结尾的C语言字符串,而是封装了一下C语言字符串并命名为sds(simple dynamic string),在sds.h文件里我们可以看到如下类型定义:
typedef char *sds;
也就是说实际上sds类型就是char*类型,那sds和char*有什么区别呢?
主要区别就是:sds一定有一个所属的结构(sdshdr),这个header结构在每次创建sds时被创建,用来存储sds以及sds的相关信息(下文sds的含义仅仅是redis的字符串,sdshdr才表示sds的header)。

那为什么redis不直接使用char*呢?总结起来理由如下:

<1>、可以常数复杂度获取字符串长度

通过len属性直接获取字符串实际长度,不包括结尾的’\0’.    时间复杂度O(1)

<2>、防止缓冲区溢出

strcat()函数不能保证目的内存是足够的。

<3>、减少修改字符串导致内存重分配的次数

空间预分配策略,Redis可以减少连续执行字符串增长所需的内存重分配次数。

<4>、二进制安全

C语言中的字符串以'\0'结尾,而Redis由于使用len记录数据长度,而不是使用空字符判断字符串是否结束,所以简单动态字符串可以存储包含空字符的数据.

1.sdshdr定义
sdshdr和sds是一一对应的关系,一个sds一定会有一个sdshdr用来记录sds的信息。在redis3.2分支出现之前sdshdr只有一个类型,定义如下:

struct sdshdr {unsigned int len;//表示sds当前的长度unsigned int free;//已为sds分配的长度-sds当前的长度char buf[];//sds实际存放的位置
};

这些版本的redis每次创建一个sds 不管sds实际有多长,都会分配一个大小固定的sdshdr。根据成员len的类型可知,sds最多能存长度为2^(8*sizeof(unsigned int))的字符串。
而3.2分支引入了五种sdshdr类型,每次在创建一个sds时根据sds的实际长度判断应该选择什么类型的sdshdr,不同类型的sdshdr占用的内存空间不同。这样细分一下可以省去很多不必要的内存开销,下面是3.2的sdshdr定义:

struct __attribute__ ((__packed__)) sdshdr5 {//实际上这个类型redis不会被使用。他的内部结构也与其他sdshdr不同,直接看sdshdr8就好。unsigned char flags; //一共8位,低3位用来存放真实的flags(类型),高5位用来存放len(长度)。char buf[];//sds实际存放的位置
};
struct __attribute__ ((__packed__)) sdshdr8 {uint8_t len;//表示当前sds的长度(单位是字节)uint8_t alloc; //表示已为sds分配的内存大小(单位是字节)unsigned char flags; //用一个字节表示当前sdshdr的类型,因为有sdshdr有五种类型,所以至少需要3位来表示000:sdshdr5,001:sdshdr8,010:sdshdr16,011:sdshdr32,100:sdshdr64。高5位用不到所以都为0。char buf[];//sds实际存放的位置
};
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[];
};

首先要说明之所以sizeof(struct sdshdr8)的大小是len+alloc+flags 是因为这个struct拥有一个柔性数组成员 buf,柔性数组成员是C99之后引入的一个新feature,这里可以通过sizeof整个struct给出buf变量的偏移量,从而确定buf的位置

其次需要说明的是定义sdshdr的这部分代码用了__attribute__ ((__packed__)),这个语法不存在于任何C语言标准,是GCC的一个extension,用来告诉编译器使用最小的内存来存储sdshdr。

引用里"minimize the memory required"其实就是让编译器尽量不使用内存对齐(alignment),以避免不必要的空间浪费,但其实这么做会有时间上的开销,假设CPU总是从存储器中读取8个字节,则变量地址必须为8的倍数,为了获取一个没对齐的8字节的uint8_t数据,CPU需要执行两次内存访问 从两个8字节的内存块中取出完整的8字节数据。关于内存对齐的更多信息,《深入理解计算机系统》第三章和《程序员的自我修养》 都有非常详细的描述。但这里我们只需要知道禁用(准确地说是尽量不使用)内存对齐是redis为了节省内存开支的一种手段。

接下来分析每个成员:

len表示sds当前sds的长度(单位是字节),不包括'0'终止符,通过len直接获取字符串长度,不需要扫一遍string,这就是上文说的封装sds的理由之一;
alloc表示当前为sds分配的大小(单位是字节)(3.2以前的版本用的free是表示还剩free字节可用空间),不包括'0'终止符;
flags表示当前sdshdr的类型,声明为char 一共有1个字节(8位),仅用低三位就可以表示所有5种sdshdr类型(详见上文代码注释):

要判断一个sds属于什么类型的sdshdr,只需 flags&SDS_TYPE_MASKSDS_TYPE_n比较即可(之所以需要SDS_TYPE_MASK是因为有sdshdr5这个特例,它的高5位不一定为0,参考上面sdshdr5定义里的代码注释)

sds.h里所有给出定义的内联函数都是通过sds作为参数,通过比较flags&SDS_TYPE_MASKSDS_TYPE_n来判断该sds属于哪种类型sdshdr,再按照指定的sdshdr类型取出sds的相关信息。
例如sdslen函数:

#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T)))) //返回一个类型为T包含s字符串的sdshdr的指针
#define SDS_TYPE_5_LEN(f) ((f)>>SDS_TYPE_BITS)  //用sdshdr5的flags成员变量做参数返回sds的长度,这其实是一个没办法的hack
#define SDS_TYPE_BITS 3 
static inline size_t sdslen(const sds s) {unsigned char flags = s[-1]; //sdshdr的flags成员变量switch(flags&SDS_TYPE_MASK) {case SDS_TYPE_5:return SDS_TYPE_5_LEN(flags);case SDS_TYPE_8:return SDS_HDR(8,s)->len;//取出sdshdr的len成员case SDS_TYPE_16:return SDS_HDR(16,s)->len;case SDS_TYPE_32:return SDS_HDR(32,s)->len;case SDS_TYPE_64:return SDS_HDR(64,s)->len;}return 0;
}

第一行里的双井号##的意思是在一个宏(macro)定义里连接两个子串(token),连接之后这##号两边的子串就被编译器识别为一个。
sdslen函数里第一行出现了s[-1],看起来感觉会是一个undefined behavior,其实不是,这是一种正常又正确的使用方式,它就等同于*(s-1)。The definition of the subscript operator [] is that E1[E2] is identical to (*((E1)+(E2))). --C99。又因为s是一个sds(char*)所以s指向的类型是char,-1就是-1*sizeof(char),由于sdshdr结构体内禁用了内存对齐,所以这也刚好是一个flags(unsigned char)的地址,所以通过s[-1]我们可以获得sds所属的sdshdr的成员变量flags。
类似sdslen这样利用sds找到sdshdr类型的还有如下几个函数,就不一一分析了:

static inline size_t sdsavail(const sds s)
static inline void sdssetlen(sds s, size_t newlen)
static inline void sdsinclen(sds s, size_t inc)
static inline size_t sdsalloc(const sds s)
static inline void sdssetalloc(sds s, size_t newlen)

2.创建一个sds

 前面说的是在已有结果的情况下,根据一个sds通过flags变量来判断它的sdshdr类型。那么最开始创建一个sds时应该选用什么类型的sdshdr来存放它的信息呢?这就得根据要存储的sds的长度决定了,redis在创建一个sds之前会调用sdsReqType(size_t string_size)来判断用哪个sdshdr。该函数传递一个sds的长度作为参数,返回应该选用的sdshdr类型

static inline char sdsReqType(size_t string_size) {if (string_size < 1<<5) //小于2^5,flags成员的高5位即可表示return SDS_TYPE_5;if (string_size < 1<<8) //小于2^8,8位整数(sdshdr8里的uint8_t)即可表示string_sizereturn SDS_TYPE_8;if (string_size < 1<<16) //小于2^16,16位整数(sdshdr16里的uint16_t)即可表示string_sizereturn SDS_TYPE_16;if (string_size < 1ll<<32) /小于2^32,32位整数(sdshrd32里的uint32_t)即可表示string_size,1ll是指1long long(至少64位)的意思,如果没有ll,1就是一个int,假设int为4字节32位,1<<32就会导致undefined behavior.return SDS_TYPE_32;return SDS_TYPE_64; //若sds的长度超过2^64,则所有类型都不法表示这个sds的len
}

 知道了创建一个sds时应选用什么类型的sdshdr后我们就可以看看创建sds的函数了:

//用init指针指向的内存的内容截取initlen长度来new一个sds,这个函数是二进制安全的
sds sdsnewlen(const void *init, size_t initlen) {void *sh;//sdshdr的指针sds s; //char * s;char type = sdsReqType(initlen);//根据需要的长度决定sdshdr的类型/* Empty strings are usually created in order to append. Use type 8* since type 5 is not good at this. */if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;//如果initlen为空并且sdshdr的类型为sdshdr5,则将类型设置为sdshdr8int hdrlen = sdsHdrSize(type);//每个sdshdr类型的大小都不一样,根据类型返回sdshdr的大小以计算需要分配的空间unsigned char *fp; /* flags pointer. */sh = s_malloc(hdrlen+initlen+1);//在heap里申请一段连续的空间给sdshdr和属于它的sds,+1是因为要在尾部放置'\0'if (!init)memset(sh, 0, hdrlen+initlen+1);//如果init为空,则整个sdshdr都用0即字符'\0'初始化if (sh == NULL) return NULL;s = (char*)sh+hdrlen;//通过sdshdr指针找到sds的位置fp = ((unsigned char*)s)-1;//找到flags的位置,等同于&s[-1]switch(type) {case SDS_TYPE_5: {*fp = type | (initlen << SDS_TYPE_BITS);//initlen左移3位到高5位,给type腾出位置,和type做或运算break;}case SDS_TYPE_8: {SDS_HDR_VAR(8,s);//#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T))); 可以理解为在switch作用域下申明了一个新的局部变量sh,类型是struct sdshdr##T,跟外面的sh值一样,变量名一样,但不是一个东西。sh->len = initlen;sh->alloc = initlen;*fp = type;//设置flagsbreak;}case SDS_TYPE_16: {SDS_HDR_VAR(16,s);sh->len = initlen;sh->alloc = initlen;*fp = type;break;}case SDS_TYPE_32: {SDS_HDR_VAR(32,s);sh->len = initlen;sh->alloc = initlen;*fp = type;break;}case SDS_TYPE_64: {SDS_HDR_VAR(64,s);sh->len = initlen;sh->alloc = initlen;*fp = type;break;}}if (initlen && init)memcpy(s, init, initlen); //memcpy不会因为'\0'而停下,支持二进制数据的拷贝s[initlen] = '\0'; //不管是不是二进制数据,尾部都会加上'\0'return s;
}
static inline int sdsHdrSize(char type) {switch(type&SDS_TYPE_MASK) {case SDS_TYPE_5:return sizeof(struct sdshdr5);//之前说的柔性数组成员不会计入struct的大小,所以这个hdrsize没有包括sds的长度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的长度判断需要选用sdshdr的类型
根据sdshdr的类型用sdsHdrSize函数得到hdrlen(其实就是sizeof(struct sdshdr))
为sdshdr分配一个hdrlen+initlen+1大小的堆内存(+1是为了放置'\0',这个'\0'不计入alloc或len)
按参数填充成员变量len、alloc和type
用memcpy给sds赋值,并在尾部加上'\0'

下面是sdsMakeRoomFor的源码:

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;/* Return ASAP if there is enough space left. */if (avail >= addlen) return s;len = sdslen(s);sh = (char*)s-sdsHdrSize(oldtype);newlen = (len+addlen);if (newlen < SDS_MAX_PREALLOC)newlen *= 2;elsenewlen += SDS_MAX_PREALLOC;type = sdsReqType(newlen);/* Don't use type 5: the user is appending to the string and type 5 is* not able to remember empty space, so sdsMakeRoomFor() must be called* at every appending operation. */if (type == SDS_TYPE_5) type = SDS_TYPE_8;hdrlen = sdsHdrSize(type);if (oldtype==type) {newsh = s_realloc(sh, hdrlen+newlen+1);if (newsh == NULL) return NULL;s = (char*)newsh+hdrlen;} else {/* Since the header size changes, need to move the string forward,* and can't use realloc */newsh = s_malloc(hdrlen+newlen+1);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);}sdssetalloc(s, newlen);return s;
}

附上sdscatlen的代码: 

sds sdscatlen(sds s, const void *t, size_t len) {size_t curlen = sdslen(s);s = sdsMakeRoomFor(s,len);if (s == NULL) return NULL;memcpy(s+curlen, t, len);sdssetlen(s, curlen+len);s[curlen+len] = '\0';return s;
}

 

2.sds缩减

我粗略地在源码里找了找,缩短sds字符串的有三个函数:sdsclear、sdstrim、sdsrange,他们都不会改变alloc的大小即不会释放任何内存,这就是sds字符串内存管理的一种方式:惰性释放。额外调用sdsRemoveFreeSpace释放内存,这样就节省了每次sds缩减长度而导致的内存释放开销。
三个缩短sds的函数就不一一介绍了,有兴趣直接去代码里看就好,需要注意的这些函数里移动字符串用的memmove()是允许内存重叠的,这点跟memcpy()不一样。
下面介绍一下sdsRemoveFreeSpace,先放源码:

//这个函数压缩内存,让alloc=len。如果type变小了,则另开一片内存复制,如果type不变,则realloc
sds sdsRemoveFreeSpace(sds s) {void *sh, *newsh;char type, oldtype = s[-1] & SDS_TYPE_MASK;int hdrlen;size_t len = sdslen(s);sh = (char*)s-sdsHdrSize(oldtype);type = sdsReqType(len);hdrlen = sdsHdrSize(type);//这之后的代码就跟sdsMakeRoomFor后面的代码差不多了,释放掉多余内存并重置alloc。if (oldtype==type) {newsh = s_realloc(sh, hdrlen+len+1);if (newsh == NULL) return NULL;s = (char*)newsh+hdrlen;} else {newsh = s_malloc(hdrlen+len+1);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);}sdssetalloc(s, len);return s;
}

 总之:sds简单动态字符串的优点(重复下上面的一段提示)

<1>、可以常数复杂度获取字符串长度

通过len属性直接获取字符串实际长度,不包括结尾的’\0’.    时间复杂度O(1)

<2>、防止缓冲区溢出

strcat()函数不能保证目的内存是足够的。

<3>、减少修改字符串导致内存重分配的次数

空间预分配策略,Redis可以减少连续执行字符串增长所需的内存重分配次数。

<4>、二进制安全

C语言中的字符串以'\0'结尾,而Redis由于使用len记录数据长度,而不是使用空字符判断字符串是否结束,所以简单动态字符串可以存储包含空字符的数据.

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

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

相关文章

同时使用有线和无线

访问无线的70网段&#xff0c;172.20.70.104 本地无线IP route add 172.20.70.0 mask 255.255.255.0 172.20.70.104 metric 1 访问无线的50网段&#xff0c;172.20.70.104 本地无线IP route add 172.20.50.0 mask 255.255.255.0 172.20.70.104 metric 2 转载于:https://www.cnb…

假期第一次编程总结(改二)

7-1 打印沙漏 &#xff08;20 分&#xff09; 本题要求你写个程序把给定的符号打印成沙漏的形状。例如给定17个“*”&#xff0c;要求按下列格式打印 所谓“沙漏形状”&#xff0c;是指每行输出奇数个符号&#xff1b;各行符号中心对齐&#xff1b;相邻两行符号数差2&#xff1…

high-speed A/D performance metrics and Amplifie...

2019独角兽企业重金招聘Python工程师标准>>> High-Speed A/D performance metrics 在高速情况下&#xff0c;主要考虑信号参数的频率范围、失真和噪声。During system definition&#xff0c;setting time 、 acquisition time、static precision-related&#xff0…

Redis之整数集合intset

intset是Redis集合的底层实现之一&#xff0c;当存储整数集合并且数据量较小的情况下Redis会使用intset作为set的底层实现。当数据量较大或者集合元素为字符串时则会使用dict实现set。 intset将整数元素按顺序存储在数组里&#xff0c;并通过二分法降低查找元素的时间复杂度。数…

场景编辑器的草案

Jojoushi场景编辑器 整个脚本的演示和编辑以点击事件为基本的单元&#xff0c;一次点击事件的生命期是&#xff1a;玩家点击一次鼠标到下一次有效的点击鼠标之间的这段时间。 1&#xff0e;显示场景 显示模型存在的场景&#xff0c;可以通过上下左右案件在场景中漫游。 2&…

c++学习书籍推荐《超越C++标准库:Boost库导论》下载

《超越C标准库Boost库导论》不仅介绍了Boost库的功能、使用方法及注意事项&#xff0c;而且还深入讨论了Boost库的设计理念、解决问题的思想和技巧以及待处理的问题。因此&#xff0c;本书是一本了解Boost库并探索其机理的实用手册。 百度云及其他网盘下载地址&#xff1a;点我…

批量替换 MySQL 指定字段中的字符串

批量替换 MySQL 指定字段中的字符串 批量替换 MySQL 指定字段中的字符串是数据库应用中很常见的需求&#xff0c;但是有很多初学者在遇到这种需求时&#xff0c;通常都是用脚本来实现&#xff1b;其实&#xff0c;MySQL 内置的有批量替换语法&#xff0c;效率也会高很多&#x…

WebCombo

原文来自方案网 http://www.fanganwang.com/Product-detail-item-1162.html&#xff0c;欢迎转载。 关键字&#xff1a; WebCombo.NET 是一款基于AJAX技术&#xff0c;处于行业领先地位的Combo box控件。它提供众多高级的数据输入功能及其独特的技术。通过其内置的数据过滤功能…

Redis之压缩列表ziplist

Redis是基于内存的nosql&#xff0c;有些场景下为了节省内存redis会用“时间”换“空间”。 ziplist就是很典型的例子。 ziplist是list键、hash键以及zset键的底层实现之一&#xff08;3.0之后list键已经不直接用ziplist和linkedlist作为底层实现了&#xff0c;取而代之的是qu…

动态链表与静态链表

一. 静态链表 在某些语言中指针是不被支持的,只能使用数组来模拟线性链表的结构.在数组中每个元素不但保存了当前元素的值,还保存了一个”伪指针域”,一般是int类型,用于指向下一个元素的内存地址. #define MAXSIZE 100; typedef struct{ ElemType data; in…

Mysql索引机制B+Tree

1、问题引入    有一个用户表&#xff0c;为了查询的效率&#xff0c;需要基于id去构建索引。构建索引我们需要考虑两个方面的问题&#xff0c;1个是查询的效率&#xff0c;1个是索引数据的存储问题。该表的记录需要支持百万、千万、甚至上亿的数据量&#xff0c;如果将索引…

GLSL学习笔记

GLSL语言内置的变量&#xff0c;包括内置的顶点属性&#xff08;attribute&#xff09;、一致变量&#xff08;uniform&#xff09;、易变变量&#xff08;varying&#xff09;以及常量&#xff08;const&#xff09;&#xff0c;一方面加深印象&#xff0c;另一方面今天的文章…

redis源码之main()函数剖析

今天看了redis的源码之中的main()函数&#xff0c;将大概的流程进行梳理。 在代码中进行了相应的注释&#xff0c;便于阅读者理解&#xff1a; int main(int argc, char **argv) {struct timeval tv;int j;#ifdef REDIS_TESTif (argc 3 && !strcasecmp(argv[1], &qu…

SQL Server 文件规划 -位置规划

数据库设计中&#xff0c;文件规划是相当重要的一个环节。 这部分内容包括文件数目的规划&#xff0c;大小的规划&#xff0c;位置的规划。 本篇介绍的是文件位置的规划&#xff0c;如下图所示 1. 数据文件 有可能的情况下&#xff0c;应该区分经常更新的表与不经常更新的表。分…

const关键字

const关键字const是constant的简写&#xff0c;只要一个变量前面用const来修饰&#xff0c;就意味着该变量里的数据可以被访问&#xff0c;不能被修改。也就是说const意味着“只读”readonly规则&#xff1a;const离谁近&#xff0c;谁就不能被修改&#xff1b;const修饰一个变…

深入理解计算机系统 第十二章 并发编程

如果逻辑控制流在时间上重叠&#xff0c;那么它们就是并发的&#xff08;concurrent&#xff09; 这种常见的现象称为并发&#xff08;concurrency&#xff09;&#xff0c;出现在计算机系统的许多不同层面上。 并发不仅仅局限于内核&#xff0c;它也可以在应用程序中扮演重要角…

Redis源码分析之小型测试框架testhelp.h和redis-check-aof.c日志检测

使用的是redis 3.2版本 test中的文件主要分为以下几个&#xff1a; 1.memtest.c 内存检测 2.redis_benchmark.c 用于redis性能测试的实现&#xff0c;后续会当做单独的一个章节进行分析 3.redis_check_aof.c 用于更新日志检查的实现。 4.redis_check_dump.c 用于本地数据库检查…

VSTS学习和迁移(1) 安装部署

要将开发环境从SVN到VFS中&#xff0c;下个月开始迁移。 先从WebCast中下载文件&#xff0c;看了安装部署部分。以下为部分截图&#xff1a; 一&#xff1a;课程内容 Team Foundation 的逻辑结构与物理结构 Team Foundation 系统要求 Team Foundation 安装实战 Team Founda…

【HNOI2013】数列

题面 题解 设\(\{a_n\}\)为差分数组&#xff0c;可以得到柿子&#xff1a;\[ \begin{aligned} ans & \sum_{a_1 1} ^ m \sum_{a_2 1} ^ m \cdots \sum_{a_{k-1} 1} ^ m (n - \sum_{i 1} ^ {k - 1} a_i) \\ & nm^{k - 1} - \sum_{a_1 1} ^ m \sum_{a_2 1} ^ m \cd…

程序员的艺术:排序算法舞蹈

1、冒泡排序&#xff1a; 2、希尔排序&#xff1a; 3、选择排序&#xff1a; 4&#xff1a;插入排序&#xff1a; 5、快速排序&#xff1a; 6、归并排序&#xff1a; 转载于:https://www.cnblogs.com/jxgxy/archive/2012/08/20/2648210.html