热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

redis学习sds字符串

redis学习-sds字符串Redis设计与实现:如果想要知道redis底层,这本书可以给予不少的帮助,非常推荐每一位学习redis的同学去翻一翻。sds字符串建议多看看源代码的实


redis学习 - sds字符串


Redis 设计与实现 :如果想要知道redis底层,这本书可以给予不少的帮助,非常推荐每一位学习redis的同学去翻一翻。


sds字符串建议多看看源代码的实现,这篇文章基本是个人看了好几篇文章之后的笔记。


源代码文件分别是: sds.csds.h


redis的string API使用


首先看下API的简单应用,设置str1变量为helloworld,然后我们使用 debug object +变量名 的方式看下,注意编码为 embstr


127.0.0.1:17100> set str1 helloworld
-> Redirected to slot [5416] located at 127.0.0.1:17300
OK
127.0.0.1:17300> debug object str1
Value at:0x7f2821c0e340 refcount:1 encoding:embstr serializedlength:11 lru:14294151 lru_seconds_idle:8

如果我们将str2设置为 helloworldhelloworldhelloworldhelloworldhell ,字符长度为44,再使用下 debug object+变量名 的方式看下,注意编码为 embstr


127.0.0.1:17300> set str2 helloworldhelloworldhelloworldhelloworldhell
-> Redirected to slot [9547] located at 127.0.0.1:17100
OK
127.0.0.1:17100> get str2
"helloworldhelloworldhelloworldhelloworldhell"
127.0.0.1:17100> debug object str2
Value at:0x7fd75e422c80 refcount:1 encoding:embstr serializedlength:21 lru:14294260 lru_seconds_idle:6

但是当我们把设置为 helloworldhelloworldhelloworldhelloworldhello ,字符长度为45,再使用 debug object+变量名 的方式看下,注意编码改变了,变为 raw


127.0.0.1:17100> set str2 helloworldhelloworldhelloworldhelloworldhello
OK
127.0.0.1:17100> debug object str2
Value at:0x7fd75e430c60 refcount:1 encoding:raw serializedlength:21 lru:14294358 lru_seconds_idle:9

最后我们将其设置为整数100,再使用 debug object+变量名 的方式看下,编码的格式变为了int。


127.0.0.1:17100> set str2 11
OK
127.0.0.1:17100> get str2
"11"
127.0.0.1:17100> debug object str2
Value at:0x7fd75e44d370 refcount:2147483647 encoding:int serializedlength:2 lru:14294440 lru_seconds_idle:9

所以Redis的string类型一共有三种存储方式:



  1. 当字符串长度小于等于44,底层采用 embstr

  2. 当字符串长度大于44,底层采用 raw

  3. 当设置是 整数 ,底层则采用 int


至于这三者有什么区别,可以直接看书:


http://redisbook.com/preview/...


为什么redis string 要使用sds字符串?



  1. O(1)获取长度 ,c语言的字符串本身不记录长度,而是通过末尾的 \0 作为结束标志,而sds本身记录了字符串的长度所以获取直接变为O(1)的时间复杂度、同时,长度的维护操作由sds的本身api实现

  2. 防止缓冲区溢出bufferoverflow :由于c不记录字符串长度,相邻字符串容易发生缓存溢出。sds在进行添加之前会检查长度是否足够,并且不足够会自动根据api扩容

  3. 减少字符串修改的内存分配次数 :使用动态扩容的机制,根据字符串的大小选择合适的header类型存储并且根据实际情况动态扩展。

  4. 使用 空间预分配和惰性空间释放 ,其实就是在扩容的时候,根据大小额外扩容2倍或者1M的空间,方面字符串修改的时候进行伸缩

  5. 使用 二进制保护 ,数据的读写不受特殊的限制,写入的时候什么样读取就是什么样

  6. 支持 兼容部分 的c字符串函数,可以减少部分API的开发


SDS字符串和C语言字符串库有什么区别


摘自黄健宏大神的一张表































C 字符串 SDS
获取字符串长度的复杂度为 O(N) 。 获取字符串长度的复杂度为 O(1) 。
API 是不安全的,可能会造成缓冲区溢出。 API 是安全的,不会造成缓冲区溢出。
修改字符串长度 N 次必然需要执行 N 次内存重分配。 修改字符串长度 N 次最多需要执行 N 次内存重分配。
只能保存文本数据。 可以保存文本或者二进制数据。
可以使用所有 库中的函数。 可以使用一部分 库中的函数。

redis的sds是如何实现的


由于c语言的string是以 \0 结尾的Redis单独封装了SDS简单动态字符串结构,如果在字符串变量十分多的情况下,会浪费十分多的内存空间,同时为了减少malloc操作,redis封装了自己的sds字符串。


下面是网上查找的一个sds字符串实现的数据结构设计图:



s1,s2分别指向真实数据区域的头部,而要确定一个sds字符串的类型,则需要通过 s[-1] 来获取对应的flags,根据flags辨别出对应的Header类型,获取到Header类型之后,根据最低三位获取header的类型(这也是使用 __attribute__ ((__packed__)) 关键字的原因下文会说明):



  • 由于s1[-1] == 0x01 == SDS_TYPE_8,因此s1的header类型是sdshdr8。

  • 由于s2[-1] == 0x02 == SDS_TYPE_16,因此s2的header类型是sdshdr16。


下面的部分是sds的实现源代码:


sds一共有5种类型的header。之所以有5种,是为了能让不同长度的字符串可以使用不同大小的header。这样,短字符串就能使用较小的header,从而节省内存。


typedef char *sds;

// 这个比较特殊,基本上用不到
struct __attribute__ ((__packed__)) sdshdr5 {
usigned char flags;
char 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[];
};
//string_size <1ll<<32
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len;
uint32_t alloc;
unsigned char flags;
char buf[];
};
//string_size <1ll<<32
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len;
uint64_t alloc;
unsigned char flags;
char buf[];
};
// 定义了五种header类型,用于表示不同长度的string
#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
#define SDS_TYPE_MASK 7 // 类型掩码
#define SDS_TYPE_BITS 3 //
#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T))); // 获取header头指针
#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T)))) // 获取header头指针
#define SDS_TYPE_5_LEN(f) ((f)>>SDS_TYPE_BITS)

上面的代码需要注意以下两个点:



  • __attribute__ ((__packed__)) 这是C语言的一种关键字,这将使这个结构体在内存中不再遵守字符串对齐规则,而是以内存紧凑的方式排列。目的时在指针寻址的时候,可以直接通过sds[-1]找到对应flags,有了flags就可以知道头部的类型,进而获取到对应的len,alloc信息,而不使用内存对齐,CPU寻址就会变慢,同时如果不对齐会造成CPU进行优化导致空白位不补0使得header和data不连续,最终无法通过flags获取低3位的header类型。

  • SDS_HDR_VAR 函数则通过结构体类型与字符串开始字节,获取到动态字符串头部的开始位置,并赋值给sh指针。 SDS_HDR 函数则通过类型与字符串开始字节,返回动态字符串头部的指针。

  • 在各个header的定义中最后有一个char buf[]。我们注意到这是一个没有指明长度的字符数组,这是C语言中定义字符数组的一种特殊写法,称为柔性数组( flexible array member ),只能定义在一个结构体的最后一个字段上。它在这里只是起到一个标记的作用,表示在flags字段后面就是一个字符数组,或者说,它指明了紧跟在flags字段后面的这个字符数组在结构体中的偏移位置。而程序在为header分配的内存的时候,它并不占用内存空间。如果计算sizeof(struct sdshdr16)的值,那么结果是5个字节,其中没有buf字段。



关于柔性数组的介绍:

深入浅出C语言中的柔性数组




  • sdshdr5 与其它几个header结构不同,它不包含alloc字段,而长度使用flags的 高5位 来存储。因此,它不能为字符串分配空余空间。如果字符串需要动态增长,那么它就必然要重新分配内存才行。所以说,这种类型的sds字符串更适合存储静态的短字符串(长度小于32)。


同时根据上面的结构可以看到,SDS结构分为两个部分:



  • len、alloc、flags。只是 sdshdr5 有所不同,



    • len: 表示字符串的真正长度(不包含NULL结束符在内)。

    • alloc: 表示字符串的最大容量(不包含最后多余的那个字节)。

    • flags: 总是占用一个字节。其中的最低3个bit用来表示header的类型。header的类型共有5种,在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


  • buf[] :柔性数组,之前有提到过,其实就是具体的数据存储区域,注意这里实际存储的数据的时候末尾存在 NULL


小贴士:


\#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))


\#号有什么作用?


这个的含义是让"#"后面的变量按照 普通字符串 来处理


双\#又有什么用处呢?


双“#”号可以理解为,在单“#”号的基础上,增加了连接功能


sds的创建和销毁


sds sdsnewlen(const void *init, size_t initlen) {
void *sh;
sds s;

char type = sdsReqType(initlen);
/* 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;
int hdrlen = sdsHdrSize(type);
unsigned char *fp; /* flags pointer. */
sh = s_malloc(hdrlen+initlen+1);
if (!init)
memset(sh, 0, hdrlen+initlen+1);
if (sh == NULL) return NULL;
s = (char*)sh+hdrlen;
fp = ((unsigned char*)s)-1;
switch(type) {
case SDS_TYPE_5: {
*fp = type | (initlen < break;
}
case SDS_TYPE_8: {
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;
}
}
if (initlen && init)
memcpy(s, init, initlen);
s[initlen] = '\0';
return s;
}
sds sdsempty(void) {
return sdsnewlen("",0);
}
sds sdsnew(const char *init) {
// 如果initlen 为NULL,使用0作为初始化数据
size_t initlen = (init == NULL) ? 0 : strlen(init);
return sdsnewlen(init, initlen);
}
void sdsfree(sds s) {
if (s == NULL) return;
s_free((char*)s-sdsHdrSize(s[-1]));
}

上面的源代码需要注意如下几个点:



  1. SDS_TYPE_5 由于设计之初按照常量对待,实际情况大多数为append操作扩容,而 SDS_TYPE_5 扩容会造成内存的分配,所以使用 SDS_TYPE_8 进行判定

  2. SDS字符串的长度为: hdrlen+initlen+1 -> sds_header 的长度 + 初始化长度 + 1 (末尾占位符 NULL 判定字符串结尾)

  3. s[initlen] = '\0'; 字符串末尾会使用 \0 进行结束标志:代表为 NULL

  4. sdsfree释放sds字符串需要计算出Header的起始位置,具体为 s_malloc 指针所指向的位置


知道了sds如何创建之后,我们可以了解一下里面调用的具体函数。比如 sdsReqTypesdsReqType 方法定义了获取类型的方法,首先根据操作系统的位数根据判别 LLONG_MAX 是否等于 LONG_MAX ,根据机器确定为32位的情况下分配sds32,同时在64位的操作系统上根据判断小于2^32分配sds32,否则分配sds64。


这里值得注意的是: string_size <1ll<<32 这段代码在 redis3.2 中才进行了bug修复,在早期版本当中这里存在分配类型的 Bug


commit


static inline char sdsReqType(size_t string_size) {
if (string_size <1<<5)
return SDS_TYPE_5;
if (string_size <1<<8)
return SDS_TYPE_8;
if (string_size <1<<16)
return SDS_TYPE_16;
// 在一些稍微久远一点的文章上面没有这一串代码 #
#if (LONG_MAX == LLONG_MAX)
if (string_size <1ll<<32)
return SDS_TYPE_32;
return SDS_TYPE_64;
#else
return SDS_TYPE_32;
#endif
}

再来看下 sdslen 方法, s[-1] 用于向低位地址偏移一个字节,和 SDS_TYPE_MASK 按位与的操作,获得Header类型,


static inline size_t sdslen(const sds s) {
unsigned char flags = s[-1];
// SDS_TYPE_MASK == 7
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;
}

sds的连接(追加)操作


/* Append the specified binary-safe string pointed by 't' of 'len' bytes to the
* end of the specified sds string 's'.
*
* After the call, the passed sds string is no longer valid and all the
* references must be substituted with the new pointer returned by the call. */
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);
// 注意末尾需要设置占位符\0代表结束标志
s[curlen+len] = '\0';
return s;
}
sds sdscat(sds s, const char *t) {
return sdscatlen(s, t, strlen(t));
}
sds sdscatsds(sds s, const sds t) {
return sdscatlen(s, t, sdslen(t));
}
// sds实现的一个核心代码,用于判别是否可以追加以及是否有足够的空间
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);
// 如果小于 1M,则分配他的两倍大小,否则分配 +1M
if (newlen newlen *= 2;
else
newlen += 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. */
// sdsheader5 会造成内存的重新分配,使用header8替代
if (type == SDS_TYPE_5) type = SDS_TYPE_8;
hdrlen = sdsHdrSize(type);
// 如果不需要重新分配header,那么试着在原来的alloc空间分配内存
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 */
// 如果需要更换Header,则需要进行数据的搬迁
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;
}

通过上面的函数可以了解到每次传入的都是一个旧变量,但是在返回的时候,都是 新的sds变量 ,redis多数的数据结构都采用这种方式处理。


同时我们可以确认一下几个点:



  • append 操作的实现函数为 sdscatlen 函数

  • getrange 这种截取一段字符串内容的方式可以直接操作字符数组,对于部分内容操作看起来比较容易,效率也比较高。


sds字符串 的空间扩容策略:


简单来说就是:



  • 小于1M,扩容修改后长度2倍

  • 大于1M,扩容1M


结尾:


多多翻翻资料,其实很多东西不需要去钻研细节,有很多优秀的人为我们答疑解惑,所以最重要的是理解作者为什么要这样设计,学习任何东西都要学习思想,思想层面的东西才是最有价值的。


sds已经被许多大佬文章进行过说明,这篇文章只是简单的归纳了一下自己看的内容, 如果有错误的地方还望指正 ,谢谢


参考资料:


下面是个人学习sds的参考资料:


Redis内部数据结构详解(2)——sds


http://zhangtielei.com/posts/...


解析redis的字符串sds数据结构:


https://blog.csdn.net/wuxing2...


SDS 与 C 字符串的区别


http://redisbook.com/preview/...


Redis源码剖析--动态字符串SDS


https://zhuanlan.zhihu.com/p/...


C基础 带你手写 redis sds


https://www.lagou.com/lgeduar...


redis源码分析系列文章


http://www.soolco.com/post/73...


Redis SDS (简单动态字符串) 源码阅读


https://chenjiayang.me/2018/0...




推荐阅读
  • 浅析python实现布隆过滤器及Redis中的缓存穿透原理_python
    本文带你了解了位图的实现,布隆过滤器的原理及Python中的使用,以及布隆过滤器如何应对Redis中的缓存穿透,相信你对布隆过滤 ... [详细]
  • 本文回顾了作者初次接触Unicode编码时的经历,并详细探讨了ASCII、ANSI、GB2312、UNICODE以及UTF-8和UTF-16编码的区别和应用场景。通过实例分析,帮助读者更好地理解和使用这些编码。 ... [详细]
  • 单片微机原理P3:80C51外部拓展系统
      外部拓展其实是个相对来说很好玩的章节,可以真正开始用单片机写程序了,比较重要的是外部存储器拓展,81C55拓展,矩阵键盘,动态显示,DAC和ADC。0.IO接口电路概念与存 ... [详细]
  • 单片机入门指南:基础理论与实践
    本文介绍了单片机的基础知识及其应用。单片机是一种将微处理器(类似于CPU)、存储器(类似硬盘和内存)以及多种输入输出接口集成在一块硅片上的微型计算机系统。通过详细解析其内部结构和功能,帮助初学者快速掌握单片机的基本原理和实际操作方法。 ... [详细]
  • Java Socket 关键参数详解与优化建议
    Java Socket 的 API 虽然被广泛使用,但其关键参数的用途却鲜为人知。本文详细解析了 Java Socket 中的重要参数,如 backlog 参数,它用于控制服务器等待连接请求的队列长度。此外,还探讨了其他参数如 SO_TIMEOUT、SO_REUSEADDR 等的配置方法及其对性能的影响,并提供了优化建议,帮助开发者提升网络通信的稳定性和效率。 ... [详细]
  • Web开发框架概览:Java与JavaScript技术及框架综述
    Web开发涉及服务器端和客户端的协同工作。在服务器端,Java是一种优秀的编程语言,适用于构建各种功能模块,如通过Servlet实现特定服务。客户端则主要依赖HTML进行内容展示,同时借助JavaScript增强交互性和动态效果。此外,现代Web开发还广泛使用各种框架和库,如Spring Boot、React和Vue.js,以提高开发效率和应用性能。 ... [详细]
  • 零拷贝技术是提高I/O性能的重要手段,常用于Java NIO、Netty、Kafka等框架中。本文将详细解析零拷贝技术的原理及其应用。 ... [详细]
  • 最详尽的4K技术科普
    什么是4K?4K是一个分辨率的范畴,即40962160的像素分辨率,一般用于专业设备居多,目前家庭用的设备,如 ... [详细]
  • 本文总结了在SQL Server数据库中编写和优化存储过程的经验和技巧,旨在帮助数据库开发人员提升存储过程的性能和可维护性。 ... [详细]
  • 您的数据库配置是否安全?DBSAT工具助您一臂之力!
    本文探讨了Oracle提供的免费工具DBSAT,该工具能够有效协助用户检测和优化数据库配置的安全性。通过全面的分析和报告,DBSAT帮助用户识别潜在的安全漏洞,并提供针对性的改进建议,确保数据库系统的稳定性和安全性。 ... [详细]
  • 本文深入解析了JDK 8中HashMap的源代码,重点探讨了put方法的工作机制及其内部参数的设定原理。HashMap允许键和值为null,但键为null的情况只能出现一次,因为null键在内部通过索引0进行存储。文章详细分析了capacity(容量)、size(大小)、loadFactor(加载因子)以及红黑树转换阈值的设定原则,帮助读者更好地理解HashMap的高效实现和性能优化策略。 ... [详细]
  • 深入解析Android 4.4中的Fence机制及其应用
    在Android 4.4中,Fence机制是处理缓冲区交换和同步问题的关键技术。该机制广泛应用于生产者-消费者模式中,确保了不同组件之间高效、安全的数据传输。通过深入解析Fence机制的工作原理和应用场景,本文探讨了其在系统性能优化和资源管理中的重要作用。 ... [详细]
  • 在Android平台中,播放音频的采样率通常固定为44.1kHz,而录音的采样率则固定为8kHz。为了确保音频设备的正常工作,底层驱动必须预先设定这些固定的采样率。当上层应用提供的采样率与这些预设值不匹配时,需要通过重采样(resample)技术来调整采样率,以保证音频数据的正确处理和传输。本文将详细探讨FFMpeg在音频处理中的基础理论及重采样技术的应用。 ... [详细]
  • 本文详细介绍了批处理技术的基本概念及其在实际应用中的重要性。首先,对简单的批处理内部命令进行了概述,重点讲解了Echo命令的功能,包括如何打开或关闭回显功能以及显示消息。如果没有指定任何参数,Echo命令会显示当前的回显设置。此外,文章还探讨了批处理技术在自动化任务执行、系统管理等领域的广泛应用,为读者提供了丰富的实践案例和技术指导。 ... [详细]
  • 使用Maven JAR插件将单个或多个文件及其依赖项合并为一个可引用的JAR包
    本文介绍了如何利用Maven中的maven-assembly-plugin插件将单个或多个Java文件及其依赖项打包成一个可引用的JAR文件。首先,需要创建一个新的Maven项目,并将待打包的Java文件复制到该项目中。通过配置maven-assembly-plugin,可以实现将所有文件及其依赖项合并为一个独立的JAR包,方便在其他项目中引用和使用。此外,该方法还支持自定义装配描述符,以满足不同场景下的需求。 ... [详细]
author-avatar
条条大路种西瓜
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有