转载: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记录数据长度,而不是使用空字符判断字符串是否结束,所以简单动态字符串可以存储包含空字符的数据.

Redis之简单动态字符串sds相关推荐

  1. Redis数据结构——简单动态字符串-SDS

    1.SDS简介: redis没有使用C语言传统的字符串表示(以空字符结尾的字符数组),而是自己构建了一种名为简单动态字符串(SDS)的抽象类型,并将SDS用作redis的默认字符串表示. 除了用来保存 ...

  2. Redis内部数据结构详解之简单动态字符串(sds)

    本文所引用的源码全部来自Redis2.8.2版本. Redis中简单动态字符串sds数据结构与API相关文件是:sds.h, sds.c. 转载请注明,本文出自:http://blog.csdn.ne ...

  3. Redis源码初探(1)简单动态字符串SDS

    前言 现在面试可太卷了,Redis基本是必问的知识点,为了在秋招中卷过其他人(虽然我未必参加秋招),本菜鸡决定从源码层面再次学习Redis,不过鉴于本菜鸡水平有限,且没有c语言基础,本文不会对源码过于 ...

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

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

  5. redis学习 -- 简单动态字符串

    Redis没有使用C语言字符串的形式,通过'\0'作为结尾,而是使用了简单动态字符串(simple dynamic string). 当Redis使用的字符串不需要修改字符串的内容的时候,可以使用C语 ...

  6. Redis数据结构——简单动态字符串

    1.简单动态字符串 redis没有直接用C语言传统的字符串(以空字符结尾的字符数组)表示,而是自己构建了一种名为简单动态字符串(SDS)的抽象类型,并将SDS用作redis的默认字符串表示. 在red ...

  7. Redis源码剖析(十)简单动态字符串sds

    在对象系统概述中发现,好像所有和字符串有关的内容都有sds的存在,实际上,它是Redis内部对于c字符串的封装,所谓c字符串,其实就是char *,在sds.h头文件中可以清楚的看到它的定义 //sd ...

  8. Redis源码阅读笔记(1)——简单动态字符串sds实现原理

    首先,sds即simple dynamic string,redis实现这个的时候使用了一个技巧,并且C99将其收录为标准,即柔性数组成员(flexible array member),参考资料见这里 ...

  9. 【redis源码学习】simple dynamic strings(简单动态字符串 sds)

    文章目录 接 化 sds 结构分析 基本操作 创建字符串 释放字符串 sdsMakeRoomFor 扩容 小tip:`__attribute__ ((__packed__))` 发 接 阅读源码之前, ...

最新文章

  1. hbase数据读取优化_从hbase读取数据优化策略和实验对照结果
  2. JVM源码分析之System.currentTimeMillis及nanoTime原理详解
  3. Absolute C++ Chapter 3 Self-Test Exercise(3)
  4. TCC分布式实现原理及分布式应用如何保证高可用
  5. 边缘计算架构_边缘计算与开放基础架构的重要性
  6. 文件跨服务器传输_跨桌面设备传输文件的最优选?
  7. TensorFlow下载与安装
  8. 03-13 微信小程序自动化测试
  9. C#之重定向输入输出
  10. module.exports与exports,export与export defa
  11. php将一个日期字符串转换成举例来说当前的,PHP将一个日期字符串转换成举例来说当前的天数...
  12. java创建集合有的不用泛型_Java如何创建泛型集合?
  13. CMMI认证的周期是多久?费用是多少?
  14. 一文读懂JPEG算法!附C++代码实现JPEG算法,实现从BMP到JPEG转换!
  15. Hive 函数之 Rank 函数案例
  16. 2017杭州云栖大会精华PPT
  17. github python100天_GitHub - CherryXuan/Python-100-Days: Python - 100天从新手到大师
  18. 阿里云服务器访问windows下网页(内网穿透)
  19. Failed to download repo mpvue/mpvue-quickstart:tunneling socket co uld not be established
  20. python3常用标准库

热门文章

  1. python爬虫从入门到放弃(六)之 BeautifulSoup库的使用
  2. AppDelegate.h
  3. Windows:chm 文件打开出现“已取消到该网页的导航”的解决方案
  4. VMware vCenter Converter 关闭SSL加密,提高35-40%性能
  5. 读书笔记《集体智慧编程》Chapter 5 : Optimization
  6. [JavaScript]return false;和e.preventDefault();的区别
  7. Windows 7 :微软目前最好的操作系统
  8. php按照文件名字排序,php readdir 排序问题,如何按照日期进行排序
  9. 进制转换中dbho是什么意思_什么是网段?二进制十进制如何互相转换?看完这篇,你就全明白了...
  10. php角色权限安全,php – 安全的chmod权限?