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

//sds.h
typedef char *sds;

所以创建sds类型变量实际上就是创建了一个char*变量,不过Redis字符串独特的地方在于它记录了字符串的长度。考虑想要计算c原生字符串的长度,需要使用strlen函数,该函数会遍历字符串直到遇到’\0’,复杂度是O(n),这容易造成性能瓶颈,所以Redis在创建字符串时记录了字符串的长度,只需要O(1)即可计算得到

字符串结构

Redis字符串不仅仅保存实际的数据,还保存着用于记录长度的头部信息。假设要创建一个长度为n的字符串,那么Redis会申请大于n的内存空间,其中,后半部分存储字符串数据,前半部分保存字符串头部信息,Redis会根据字符串的长度选择不同的头部编码

//sds.h
/* * 字符串Header的定义,在字符串前面存放一个Header,用于记录字符串的信息* __attribute__ ((__packed__))是通知编译器使用紧凑模式分配内存* 由于内存对齐的原因,编译器可能会在任意位置添加字节以满足对齐条件* 该声明告知编译器在成员变量之间不能插入字节,要插就在头尾插* 原因是如果在中间插会破坏内存结构,导致直接进行char*加法无法准确获取数据 * */
struct __attribute__ ((__packed__)) sdshdr5 {unsigned char flags; char buf[];
};/* * 不同长度字符串采用不同的Header结构以节约内存* len : 字符串数据真正长度,已容纳的字符数,不包括结尾字符'\0'* alloc : 字符串数据的最大容量,可以容纳多少个字符,不包括结尾字符'\0'* flags : Header的类型,总共5中,但是sdshdr5已经不再使用* buf : 实际存储字符串的位置*/
struct __attribute__ ((__packed__)) sdshdr8 {uint8_t len; uint8_t alloc; unsigned char flags; char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {uint16_t len; uint16_t alloc; unsigned char flags; char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {uint32_t len; uint32_t alloc; unsigned char flags; char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {uint64_t len; uint64_t alloc; unsigned char flags;char buf[];
};

其中,flags记录着当前头部采用的编码格式,由宏定义指出

//sds.h
#define SDS_TYPE_5  0
#define SDS_TYPE_8  1
#define SDS_TYPE_16 2
#define SDS_TYPE_32 3
#define SDS_TYPE_64 4

这么做的原因是为了节省内存,如果一个字符串的长度使用uint8_t就可以保存,那么使用uint64_t就显得多余了,还记得吗,Redis正在努力节省内存

前面也看到了,sds就是char*的类型别名,所以平常使用的sds实际上是指sdshdr中的buf部分,那么怎么获取头部信息呢,Redis保证为sdshdr和buf中的字符串申请的内存是连续的,也就是说如果sds指向buf的首地址,那么sds-1就指向头部信息中的flags地址,sds-n就可以获取长度和容量地址(n需要根据flags表示的编码格式计算)

由于Redis中使用长度记录字符串的结束位置,而不是依赖于’\0’,这使Redis中的字符串是二进制安全的,因为在二进制文件中,可能会存在多个’\0’,如果仅仅使用c语言原生字符串,那么很多数据都会丢失

字符串操作

创建字符串

字符串创建工作由sdsnewlen函数完成,函数首先根据字符串长度找到合适的头部编码,然后一次性申请头部和字符串数据的内存,完成初始化工作后,返回字符串数据

//sds.c
/* 申请长度为initLen的字符串,如果init不为空,那么将init的数据复制到字符串中 */
sds sdsnewlen(const void *init, size_t initlen) {void *sh;sds s;/* 根据字符串长度选择合适的头部编码 */char type = sdsReqType(initlen);/* sdshdr5已经不再使用,默认使用sdshdr8代替 */if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;/* 获取选择的头部大小,用于申请内存 */int hdrlen = sdsHdrSize(type);unsigned char *fp; /* flags pointer. *//* 宏定义,调用的是zmalloc, 加1是为了保存'\0' *//* 一次性申请可以保存头部信息和字符串数据的内存空间,确保内存是连续的 */sh = s_malloc(hdrlen+initlen+1);/* 如果给定的地址是null,那么就初始化分配的内存为0,否则,执行拷贝工作 */if (!init)memset(sh, 0, hdrlen+initlen+1);if (sh == NULL) return NULL;/* sh指向头部,为了获取字符串数据,需要跳过头部内存 * s指向实际用于保存数据的地址 */s = (char*)sh+hdrlen;/* s-1是Header中flags的地址 */fp = ((unsigned char*)s)-1;switch(type) {case SDS_TYPE_5: {*fp = type | (initlen << SDS_TYPE_BITS);break;}case SDS_TYPE_8: {/* 获取不同类型的Header的指针 */SDS_HDR_VAR(8,s);/* 设置属性,首次申请的容量和数据长度相等 */sh->len = initlen;sh->alloc = initlen;*fp = type;break;}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;}}/* 如果给定地址不为null,执行拷贝工作 */if (initlen && init)memcpy(s, init, initlen);/* 设置结束字符,多申请1个字节的原因 */s[initlen] = '\0';/* 返回字符串数据部分 */return s;
}

SDS_HDR_VAR是宏定义,根据头部编码和字符串数据地址s获取头部地址,只需要数据地址减去头部长度即可,其中##用于连接左右两部分

//sds.h
/* 获取字符串Header的属性值,##作用是将前后两部分连在一起,即sdshdr##32 == sdshdr32(此时T为32) */
#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));

sdsReqType函数用于根据字符串长度选择合适的头部编码

//sds.c
/* 根据字符串长度选择合适的头部编码,需要找到足够容纳并且最小的编码格式 */
static inline char sdsReqType(size_t string_size) {/* 2的5次方,使用SDS_TYPE_5 */if (string_size < 1<<5)return SDS_TYPE_5;/* 2的8次方,使用uint8_t */if (string_size < 1<<8)return SDS_TYPE_8;/* 使用uint16_t */if (string_size < 1<<16)return SDS_TYPE_16;/* uint32_t */if (string_size < 1ll<<32)return SDS_TYPE_32;/* uint64_t */return SDS_TYPE_64;
}

获取字符串长度

获取字符串长度只需要从头部信息中读取,时间复杂度为O(1)

//sds.h
/* 获取字符串长度,先获取头部编码,然后地址前移,找到头部地址,获取长度 */
static inline size_t sdslen(const sds s) {/* 获取头部编码 */unsigned char flags = s[-1];/* 选择对应的头部编码,获取头部地址,返回长度 */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;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;
}

因为头部编码类型的宏定义分别是0,1,2,3,4,所以这里flags与类型掩码做与运算只是让flags的值在0到4之间,SDS_TYPE_MASK实际上是7

获取字符串剩余容量

获取字符串的剩余容量只需要用总容量减去已用容量,由sdsavail函数完成,函数大体和sdslen相同

//sds.h
/* * 返回剩余可利用的内存大小,利用Header中的alloc和len属性* alloc : 保存字符串的总容量* len : 实际存储的大小*/
static inline size_t sdsavail(const sds s) {unsigned char flags = s[-1];switch(flags&SDS_TYPE_MASK) {case SDS_TYPE_5: {return 0;}case SDS_TYPE_8: {SDS_HDR_VAR(8,s);//和sdslen函数相比仅仅是返回数据不同return sh->alloc - sh->len;}case SDS_TYPE_16: {SDS_HDR_VAR(16,s);return sh->alloc - sh->len;}case SDS_TYPE_32: {SDS_HDR_VAR(32,s);return sh->alloc - sh->len;}case SDS_TYPE_64: {SDS_HDR_VAR(64,s);return sh->alloc - sh->len;}}return 0;
}

字符串容量扩充

sdsMakeRoomFor函数用于将字符串扩充,扩充的目的通常用于和其他字符串拼接

//sds.c
/* 为s的字符串申请更大的容量已容纳addLen个字节(Header中的alloc记录当前字符串的总容量) */
sds sdsMakeRoomFor(sds s, size_t addlen) {void *sh, *newsh;/* 获取s字符串剩余可利用的字节大小,Header中的总容量(alloc) - 当前大小(len) */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;/* 返回s字符串的已用大小(Header中的len) */len = sdslen(s);/* 获取字符串s的头部地址 */sh = (char*)s-sdsHdrSize(oldtype);/* 新的容量等于当前已用空间加扩充大小 */newlen = (len+addlen);/* 每次扩充都会申请多余空间以备不时之需 */if (newlen < SDS_MAX_PREALLOC)newlen *= 2;elsenewlen += SDS_MAX_PREALLOC;/* 为新容量找到一个合适的头部编码 */type = sdsReqType(newlen);if (type == SDS_TYPE_5) type = SDS_TYPE_8;/* 计算新编码的Header大小 */hdrlen = sdsHdrSize(type);/* 如果新编码和之前编码相同,只需要扩充数据即可* 否则,重新申请内存(因为不同编码的Header大小不同,同时也需要扩充头部) */if (oldtype==type) {/* 在原地址处重新申请内存 */newsh = s_realloc(sh, hdrlen+newlen+1);if (newsh == NULL) return NULL;/* 获取字符串数据地址 */s = (char*)newsh+hdrlen;} else {/* 重新申请新内存 */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;/* 设置Header编码 */s[-1] = type;/* 设置字符串长度 */sdssetlen(s, len);}/* 更新字符串容量 */sdssetalloc(s, newlen);return s;
}

连接两个字符串

前面也说到了,扩充容量通常是为了拼接其他字符串,由sdscatlen实现

//sds.c
/* 连接两个字符串 */
sds sdscatlen(sds s, const void *t, size_t len) {size_t curlen = sdslen(s);/* 为s扩充容量,足以容纳字符串t(长度为len) */s = sdsMakeRoomFor(s,len);if (s == NULL) return NULL;/* 将字符串复制到s的尾部 */memcpy(s+curlen, t, len);/* 更新字符串已用容量 */sdssetlen(s, curlen+len);/* 设置结束字符 */s[curlen+len] = '\0';return s;
}

字符串容量缩减

扩充和缩减是相反的两个过程,代码也非常相似,扩充是令容量增加,缩减是令容量减小

//sds.c
/* 将字符串末尾的空闲空间释放掉,即另alloc == len */
sds sdsRemoveFreeSpace(sds s) {void *sh, *newsh;char type, oldtype = s[-1] & SDS_TYPE_MASK;int hdrlen;/* 获取当前字符串的长度len */size_t len = sdslen(s);sh = (char*)s-sdsHdrSize(oldtype);/* 找到合适的Header编码(有可能编码变小) */type = sdsReqType(len);hdrlen = sdsHdrSize(type);/* 判断Header的类型和原先是否相同,如果不相同,需要重新分配内存* 因为Hedaer大小会改变 */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

在之前对象系统的介绍中,发现很多地方用到了sds变量,现在就来回忆一下,顺便填填坑

创建Raw类型和编码的字符串对象

创建raw字符串由createRawStringObject函数完成,函数中调用sdsnewlen创建了一个sds字符串

//object.c
/* 根据type和ptr创建编码为raw字符串的对象 */
robj *createObject(int type, void *ptr) {/* 申请对象内存空间 */robj *o = zmalloc(sizeof(*o));/* 设置类型,编码,值,引用计数初始化为1 */o->type = type;o->encoding = OBJ_ENCODING_RAW;o->ptr = ptr;o->refcount = 1;/* 计算当前时间,赋值给lru作为最后一次访问时间 */o->lru = LRU_CLOCK();/* 返回对象指针 */return o;
}/* 创建raw字符串类型变量 */
robj *createRawStringObject(const char *ptr, size_t len) {/* sdsnewlen()创建一个长度为len的sds字符串 */return createObject(OBJ_STRING,sdsnewlen(ptr,len));
}

可以看到,raw字符串调用了两次动态内存申请,一次在sdsnewlen函数中申请头部和字符串数据内存,一次在createObject函数中申请robj内存

创建Embstr类型和编码的字符串对象

创建embstr字符串由createEmbeddedStringObject函数完成

//object.c
/* 创建类型为embstr,编码为embstr的字符串对象 */
robj *createEmbeddedStringObject(const char *ptr, size_t len) {/* 一次性创建robj和sds内存* 因为embstr仅仅用于长度较小的字符串,所以使用sdshdr8就足够了*/robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr8)+len+1);/* o是robj*类型,o+1实际加的字节数是sizeof(robj),得到的是sds头部地址 */struct sdshdr8 *sh = (void*)(o+1);/* 设置类型,编码,数据,引用计数,最后一次访问时间 */o->type = OBJ_STRING;o->encoding = OBJ_ENCODING_EMBSTR;o->ptr = sh+1;o->refcount = 1;o->lru = LRU_CLOCK();/* 设置sds头部信息,复制数据到sds中 */sh->len = len;sh->alloc = len;sh->flags = SDS_TYPE_8;if (ptr) {memcpy(sh->buf,ptr,len);sh->buf[len] = '\0';} else {memset(sh->buf,0,len+1);}return o;
}

可以看到,embstr字符串只调用了一次动态内存申请,一次性申请了robj,sds头部和字符串数据所需要的所有内存,所以embstr通常用于长度较小的字符串编码,因为动态内存申请是很耗时的,尤其是申请大内存时

小结

sds实际上就是char*类型,Redis在c原生字符串的基础上添加了头部信息,用于以O(1)的时间复杂度获取字符串长度。字符串操作的实现都比较简单,其中由于头部信息和数据是连续存储的,所以可以使用类似s[-n]的形式索引到头部信息

Redis源码剖析(十)简单动态字符串sds相关推荐

  1. c语言追加字符串_Redis源码解析二--简单动态字符串

    Redis 简单动态字符串 1.介绍 Redis兼容传统的C语言字符串类型,但没有直接使用C语言的传统的字符串(以'0'结尾的字符数组)表示,而是自己构建了一种名为简单动态字符串(simple dyn ...

  2. redis源码剖析(五)—— 字符串,列表,哈希,集合,有序集合

    文章目录 对象 REDIS_STRING (字符串) REDIS_LIST 列表 REDIS_SET (集合) REDIS_ZSET (有序集合) REDIS_HASH (hash表) int ref ...

  3. redis源码剖析(1):基础数据结构SDS

    目录 1.SDS概述 2.SDS的定义 2.1 3.2版本和3.0版本的差别 3.SDS数据结构解读 4.SDS重点源码分析 4.1 创建SDS 4.2 SDS拼接 4.3 SDS惰性空间释放 5.总 ...

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

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

  5. Redis源码剖析和注释(十六)---- Redis输入输出的抽象(rio)

    Redis源码剖析和注释(十六)---- Redis输入输出的抽象(rio) . https://blog.csdn.net/men_wen/article/details/71131550 Redi ...

  6. redis源码剖析(十五)——客户端思维导图整理

    redis源码剖析(十五)--客户端执行逻辑结构整理 加载略慢

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

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

  8. 【Redis源码剖析】 - Redis内置数据结构之压缩列表ziplist

    在前面的一篇文章[Redis源码剖析] - Redis内置数据结构之双向链表中,我们介绍了Redis封装的一种"传统"双向链表list,分别使用prev.next指针来指向当前节点 ...

  9. 【Redis源码剖析】 - Redis IO操作之rio

    原创作品,转载请标明:http://blog.csdn.net/xiejingfa/article/details/51433696 Redis源码剖析系列文章汇总:传送门 Reids内部封装了一个I ...

  10. 【Redis源码剖析】 - Redis持久化之RDB

    原创作品,转载请标明:http://blog.csdn.net/xiejingfa/article/details/51553370 Redis源码剖析系列文章汇总:传送门 Redis是一个高效的内存 ...

最新文章

  1. Lua中的模块与module函数详解
  2. 深入理解 C 指针阅读笔记 -- 第三章
  3. python 用途-Python在每个行业的用处
  4. 【Kotlin】Kotlin 领域特定语言 DSL 原理 一 ( DSL 简介 | 函数 / 属性扩展 )
  5. λ-矩阵(初等因子)
  6. WindowsServer2012史记5-简洁,管理更多的服务器
  7. Django之项目搭建和配置总结(一)
  8. 用Python玩连连看是什么效果?
  9. keil C51 例子
  10. Git学习(一)本地操作
  11. 爬虫项目——xpath练手(1)
  12. Zabbix 监控 MongoDB
  13. 用python进行文本分析_用Python分析文本文件
  14. 买笔记本电脑主要看什么?
  15. 1038: 绝对值最大 C语言
  16. promox VE各版本ISO下载及安装教程
  17. Java:使用Java编写一个随机点名器
  18. 视频剪辑软件实用推荐
  19. [年终总结]愿你永远清澈明朗,眼里有光
  20. 加速度传感器的应用(检测打滑)——利用传感器检测智能车加速度及速度全面解析方案

热门文章

  1. linux设备和驱动注册,Linux驱动第五篇-----驱动注册和生成设备节点
  2. wordpress php7 mysql_WordPress可以使用PHP7的MySQLi扩展
  3. linux oracle目录权限不够,Linux 目录权限不足导致ORA-39070错误 | 信春哥,系统稳,闭眼上线不回滚!...
  4. CCF201809(Java)
  5. [分治] Jzoj P5807 简单的区间
  6. 第十二章 类和动态内存分配
  7. Redis单机配置多实例,实现主从同步
  8. find 和 DOM遍历孰快孰慢~
  9. 用程序算法做人生选择
  10. JSP 中使用Struts2的值