Redis内部数据结构SDS详解

存储 存储软件 Redis
Redis是使用C写的,而C中根本不存在string,list,hash,set和zset这些数据类型,那么C是如何将这些数据类型实现出来的呢?我们从该篇开始,就要开始分析源码啦??。

本文转载自微信公众号「 学习Java的小姐姐」,作者学习Java的小姐姐0618。转载本文请联系学习Java的小姐姐公众号。

[[331487]]

前言

Redis是使用C写的,而C中根本不存在string,list,hash,set和zset这些数据类型,那么C是如何将这些数据类型实现出来的呢?我们从该篇开始,就要开始分析源码啦??。

API使用

我们这篇来学习string的底层实现,首先看下API的简单应用,设置str1变量为helloworld,然后我们使用debug object +变量名的方式看下,注意标红的编码为embstr。

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

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

最后我们将str3设置为整数100,再使用debug object+变量名的方式看下,注意标红的编码为int。

所以Redis的string类型一共有三种存储方式,当字符串长度小于等于44,底层采用embstr;当字符串长度大于44,底层采用raw;当设置是整数,底层则采用int。

embstr和raw的区别

所有类型的数据结构最外层都是RedisObject,这部分会说,先这样大致了解下,因为这篇的重点不在这。如果字符串小于等于44,实际的数据和RedisObject在内存中地址相邻,如下图。

如果字符串大于44,实际的数据和RedisObject在内存中地址不相邻,如下图。

再次强调,这些不重要,以后会讲,现在提下,只是为了能让Redis的String类型有个大致了解,先从整体把握。我们今天要说的其实是实际的数据,即上图指针指向的位置??。

SDSHdr的定义

其实的数据并不是直接存储,也有封装,看下面的代码就知道分为五种,分别是sdshdr5,sdshdr8,sdshdr16,sdshdr32,sdshdr64。sdshdr5和另外四种的区别比较明显,sdshrd5其实对内存空间的更加节约。其他四种乍一看都差不多,包括已用长度len,总长度alloc,标记flags(感觉没啥用,要是有知道的小伙伴,欢迎指教),实际数据buf。

  1. //定义五种不同的结构体,sdshdr5,sdshdr8, sdshdr16,sdshdr32,sdshdr64 
  2. struct __attribute__ ((__packed__)) sdshdr5 { 
  3.     unsigned char flags; // 8位的标记 
  4.     char buf[];//实际数据的指针 
  5. }; 
  6. struct __attribute__ ((__packed__)) sdshdr8 { 
  7.     uint8_t len; /* 已使用长度 */ 
  8.     uint8_t alloc; /* 总长度*/ 
  9.     unsigned char flags; 
  10.     char buf[]; 
  11. }; 
  12. struct __attribute__ ((__packed__)) sdshdr16 { 
  13.     uint16_t len; 
  14.     uint16_t alloc; 
  15.     unsigned char flags; 
  16.     char buf[]; 
  17. }; 
  18. struct __attribute__ ((__packed__)) sdshdr32 { 
  19.     uint32_t len; 
  20.     uint32_t alloc; 
  21.     unsigned char flags; 
  22.     char buf[]; 
  23. }; 
  24. struct __attribute__ ((__packed__)) sdshdr64 { 
  25.     uint64_t len; 
  26.     uint64_t alloc; 
  27.     unsigned char flags; 
  28.     char buf[]; 
  29. }; 

SDS具体逻辑图

假设我们设置某个字符串为hello,那么他SDS的可用长度len为8,已用长度len为6,如下图。注意:Redis会根据具体的字符长度,选择相应的sdshdr,但是各个类型都差不多,所以下图加简单画了。

SDS的优势

我们可以看到是对字符数组的再封装,但是为什么呢,直接使用字符数组不是更简单吗?这要从C和Java语言的根本区别说起。

更快速的获取字符串长度

我们都知道Java的字符串有提供length方法,列表有提供size方法,我们可以直接获取大小。但是C却不一样,更偏向底层实现,所以没有直接的方法使用。这样就带来一个问题,如果我们想要获取某个数组的长度,就只能从头开始遍历,当遇到第一个'\0'则表示该数组结束。这样的速度太慢了,不能每次因为要获取长度就变量数组。所以设计了SDS数据结构,在原来的字符数组外面增加总长度,和已用长度,这样每次直接获取已用长度即可。复杂度为O(1)。

数据安全,不会截断

如果传统字符串保存图片,视频等二进制文件,中间可能出现'\0',如果按照原来的逻辑,会造成数据丢失。所以可以用已用长度来表示是否字符数组已结束。

SDS关键代码分析

获取常见值(抽象出常见方法)

在sds.h中写了一些常见方法,比如计算sds的长度(即sdshdr的len),计算sds的空闲长度(即sdshdr的可用长度alloc-已用长度len),计算sds的可用长度(即sdshdr的alloc)等等。但是大家有没有疑问,这不是一行代码搞定的事吗,为啥要抽象出方法呢?那么问题在于在上面,我们有将sdshdr分为五种类型,分别是sdshdr5,sdshdr8,sdshdr16,sdshdr32,sdshdr64。那么我们在实际使用的时候,想要区分当前是哪个类型,并取其相应字段或设置相应字段。

  1. //计算sds对应的字符串长度,其实上取得是字符串所对应的哪种sdshdr的len值 
  2. static inline size_t sdslen(const sds s) { 
  3.     // 柔性数组不占空间,所以倒数第二位的是flags 
  4.     unsigned char flags = s[-1]; 
  5.     //flags与上面定义的宏变量7做位运算 
  6.     switch(flags&SDS_TYPE_MASK) { 
  7.         case SDS_TYPE_5://0 
  8.             return SDS_TYPE_5_LEN(flags); 
  9.         case SDS_TYPE_8://1 
  10.             return SDS_HDR(8,s)->len;//取上面结构体sdshdr8的len 
  11.         case SDS_TYPE_16://2 
  12.             return SDS_HDR(16,s)->len; 
  13.         case SDS_TYPE_32://3 
  14.             return SDS_HDR(32,s)->len; 
  15.         case SDS_TYPE_64://5 
  16.             return SDS_HDR(64,s)->len; 
  17.     } 
  18.     return 0; 
  19. //计算sds对应的空余长度,其实上是alloc-len 
  20. static inline size_t sdsavail(const sds s) { 
  21.     unsigned char flags = s[-1]; 
  22.     switch(flags&SDS_TYPE_MASK) { 
  23.         case SDS_TYPE_5: { 
  24.             return 0; 
  25.         } 
  26.         case SDS_TYPE_8: { 
  27.             SDS_HDR_VAR(8,s); 
  28.             return sh->alloc - sh->len; 
  29.         } 
  30.         case SDS_TYPE_16: { 
  31.             SDS_HDR_VAR(16,s); 
  32.             return sh->alloc - sh->len; 
  33.         } 
  34.         case SDS_TYPE_32: { 
  35.             SDS_HDR_VAR(32,s); 
  36.             return sh->alloc - sh->len; 
  37.         } 
  38.         case SDS_TYPE_64: { 
  39.             SDS_HDR_VAR(64,s); 
  40.             return sh->alloc - sh->len; 
  41.         } 
  42.     } 
  43.     return 0; 
  44. //设置sdshdr的len 
  45. static inline void sdssetlen(sds s, size_t newlen) { 
  46.     unsigned char flags = s[-1]; 
  47.     switch(flags&SDS_TYPE_MASK) { 
  48.         case SDS_TYPE_5: 
  49.             { 
  50.                 unsigned char *fp = ((unsigned char*)s)-1; 
  51.                 *fp = SDS_TYPE_5 | (newlen << SDS_TYPE_BITS); 
  52.             } 
  53.             break; 
  54.         case SDS_TYPE_8: 
  55.             SDS_HDR(8,s)->len = newlen; 
  56.             break; 
  57.         case SDS_TYPE_16: 
  58.             SDS_HDR(16,s)->len = newlen; 
  59.             break; 
  60.         case SDS_TYPE_32: 
  61.             SDS_HDR(32,s)->len = newlen; 
  62.             break; 
  63.         case SDS_TYPE_64: 
  64.             SDS_HDR(64,s)->len = newlen; 
  65.             break; 
  66.     } 
  67. //给sdshdr的len添加多少大小 
  68. static inline void sdsinclen(sds s, size_t inc) { 
  69.     unsigned char flags = s[-1]; 
  70.     switch(flags&SDS_TYPE_MASK) { 
  71.         case SDS_TYPE_5: 
  72.             { 
  73.                 unsigned char *fp = ((unsigned char*)s)-1; 
  74.                 unsigned char newlen = SDS_TYPE_5_LEN(flags)+inc; 
  75.                 *fp = SDS_TYPE_5 | (newlen << SDS_TYPE_BITS); 
  76.             } 
  77.             break; 
  78.         case SDS_TYPE_8: 
  79.             SDS_HDR(8,s)->len += inc; 
  80.             break; 
  81.         case SDS_TYPE_16: 
  82.             SDS_HDR(16,s)->len += inc; 
  83.             break; 
  84.         case SDS_TYPE_32: 
  85.             SDS_HDR(32,s)->len += inc; 
  86.             break; 
  87.         case SDS_TYPE_64: 
  88.             SDS_HDR(64,s)->len += inc; 
  89.             break; 
  90.     } 
  91. //获取sdshdr的总长度 
  92. static inline size_t sdsalloc(const sds s) { 
  93.     unsigned char flags = s[-1]; 
  94.     switch(flags&SDS_TYPE_MASK) { 
  95.         case SDS_TYPE_5: 
  96.             return SDS_TYPE_5_LEN(flags); 
  97.         case SDS_TYPE_8: 
  98.             return SDS_HDR(8,s)->alloc; 
  99.         case SDS_TYPE_16: 
  100.             return SDS_HDR(16,s)->alloc; 
  101.         case SDS_TYPE_32: 
  102.             return SDS_HDR(32,s)->alloc; 
  103.         case SDS_TYPE_64: 
  104.             return SDS_HDR(64,s)->alloc; 
  105.     } 
  106.     return 0; 
  107. //设置sdshdr的总长度 
  108. static inline void sdssetalloc(sds s, size_t newlen) { 
  109.     unsigned char flags = s[-1]; 
  110.     switch(flags&SDS_TYPE_MASK) { 
  111.         case SDS_TYPE_5: 
  112.             /* Nothing to do, this type has no total allocation info. */ 
  113.             break; 
  114.         case SDS_TYPE_8: 
  115.             SDS_HDR(8,s)->alloc = newlen; 
  116.             break; 
  117.         case SDS_TYPE_16: 
  118.             SDS_HDR(16,s)->alloc = newlen; 
  119.             break; 
  120.         case SDS_TYPE_32: 
  121.             SDS_HDR(32,s)->alloc = newlen; 
  122.             break; 
  123.         case SDS_TYPE_64: 
  124.             SDS_HDR(64,s)->alloc = newlen; 
  125.             break; 
  126.     } 

创建对象

我们通过sdsnew方法来创建对象,显示通过判断init是否为空来确定初始大小,接着调用方法sdsnew(这边方法名一样,但是参数不一样,其为方法的重载),先根据长度确定类型(上面有提过五种类型,不记得的可以往上翻),然后根据类型分配相应的内存资源,最后追加C语言的结尾符'\0'。

  1. sds sdsnew(const char *init) { 
  2.     size_t initlen = (init == NULL) ? 0 : strlen(init); 
  3.     return sdsnewlen(init, initlen); 
  4.  
  5. sds sdsnewlen(const void *init, size_t initlen) { 
  6.     void *sh; 
  7.     sds s; 
  8.     char type = sdsReqType(initlen);//根据长度确定类型 
  9.     /*空字符串,用sdshdr8,这边是经验写法,当想构造空串是为了放入超过32长度的字符串 */ 
  10.     if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8; 
  11.     int hdrlen = sdsHdrSize(type);//到下一个方法,已经把他们放在一起了 
  12.     unsigned char *fp; /* flags pointer. */ 
  13.  
  14.     //分配内存 
  15.     sh = s_malloc(hdrlen+initlen+1); 
  16.     if (!init) 
  17.         memset(sh, 0, hdrlen+initlen+1); 
  18.     if (sh == NULLreturn NULL
  19.     s = (char*)sh+hdrlen; 
  20.     fp = ((unsigned char*)s)-1; 
  21.     //根据不同的类型,创建不同结构体,调用SDS_HDR_VAR函数 
  22.     //为不同的结构体赋值,如已用长度len,总长度alloc 
  23.     switch(type) { 
  24.         case SDS_TYPE_5: { 
  25.             *fp = type | (initlen << SDS_TYPE_BITS); 
  26.             break; 
  27.         } 
  28.         case SDS_TYPE_8: { 
  29.             SDS_HDR_VAR(8,s); 
  30.             sh->len = initlen; 
  31.             sh->alloc = initlen; 
  32.             *fp = type; 
  33.             break; 
  34.         } 
  35.         case SDS_TYPE_16: { 
  36.             SDS_HDR_VAR(16,s); 
  37.             sh->len = initlen; 
  38.             sh->alloc = initlen; 
  39.             *fp = type; 
  40.             break; 
  41.         } 
  42.         case SDS_TYPE_32: { 
  43.             SDS_HDR_VAR(32,s); 
  44.             sh->len = initlen; 
  45.             sh->alloc = initlen; 
  46.             *fp = type; 
  47.             break; 
  48.         } 
  49.         case SDS_TYPE_64: { 
  50.             SDS_HDR_VAR(64,s); 
  51.             sh->len = initlen; 
  52.             sh->alloc = initlen; 
  53.             *fp = type; 
  54.             break; 
  55.         } 
  56.     } 
  57.     if (initlen && init) 
  58.         memcpy(s, init, initlen); 
  59.     //最后追加'\0' 
  60.     s[initlen] = '\0'
  61.     return s; 
  62.  
  63. //根据实际字符长度确定类型 
  64. static inline char sdsReqType(size_t string_size) { 
  65.     if (string_size < 1<<5) 
  66.         return SDS_TYPE_5; 
  67.     if (string_size < 1<<8) 
  68.         return SDS_TYPE_8; 
  69.     if (string_size < 1<<16) 
  70.         return SDS_TYPE_16; 
  71. #if (LONG_MAX == LLONG_MAX) 
  72.     if (string_size < 1ll<<32) 
  73.         return SDS_TYPE_32; 
  74. #endif 
  75.     return SDS_TYPE_64; 

删除

String类型的删除并不是直接回收内存,而是修改字符,让其为空字符,这其实是惰性释放,等待将来使用。在调用sdsempty方法时,再次调用上面的sdsnewlen方法。

  1. /*修改sds字符串使其为空(零长度)。 
  2.  
  3. *但是,所有现有缓冲区不会被丢弃,而是设置为可用空间 
  4.  
  5. *这样,下一个append操作将不需要分配到 
  6.  
  7. *当要缩短SDS保存的字符串时,程序并不立即使用内存充分配来回收缩短后多出来的字节,并等待将来使用。*/ 
  8. void sdsclear(sds s) { 
  9.     sdssetlen(s, 0); 
  10.     s[0] = '\0'
  11.  
  12. sds sdsempty(void) { 
  13.     return sdsnewlen("",0); 

添加字符(扩容)重点!!!

添加字符串,sdscat输入参数为sds和字符串t,首先调用sdsMakeRoomFor扩容方法,再追加新的字符串,最后添加上结尾符'\0'。我们来看下扩容方法里面是如何实现的?第一步先调用常见方法中的sdsavail方法,获取还剩多少空闲空间。如果空闲空间大于要添加的字符串t的长度,则直接返回,不想要扩容。如果空闲空间不够,则想要扩容。第二步判断想要扩容多大,这边有分情况,如果目前的字符串小于1M,则直接扩容双倍,如果目前的字符串大于1M,则直接添加1M。第三个判断添加字符串之后的数据类型还是否和原来的一致,如果一致,则没啥事。如果不一致,则想要新建一个sdshdr,把现有的数据都挪过去。

这样是不是有点抽象,举个例子,现在str的字符串为hello,目前是sdshdr8,总长度50,已用6,空闲44。现在想要添加长度为50的字符t,第一步想要看下是否要扩容,50明显大于44,需要扩容。第二步扩容多少,str的长度小于1M,所以扩容双倍,新的长度为50*2=100。第三步50+50所对应sdshdr类型还是sdshdr8吗?明显还是sdshdr8,所以不要数据迁移,还在原来的基础上添加t即可。

  1. sds sdscat(sds s, const char *t) { 
  2.     return sdscatlen(s, t, strlen(t)); 
  3.  
  4. sds sdscatlen(sds s, const void *t, size_t len) { 
  5.     //调用sds.h里面的sdslen,即取已用长度 
  6.     size_t curlen = sdslen(s); 
  7.     //扩容方法 
  8.     s = sdsMakeRoomFor(s,len); 
  9.     if (s == NULLreturn NULL
  10.     memcpy(s+curlen, t, len); 
  11.     sdssetlen(s, curlen+len); 
  12.     s[curlen+len] = '\0'
  13.     return s; 
  14.  
  15. sds sdsMakeRoomFor(sds s, size_t addlen) { 
  16.     void *sh, *newsh; 
  17.     //调用sds.h,获取空闲长度alloc 
  18.     size_t avail = sdsavail(s); 
  19.     size_t len, newlen; 
  20.     char type, oldtype = s[-1] & SDS_TYPE_MASK; 
  21.     int hdrlen; 
  22.  
  23.    //空闲长度大于需要增加的,不需要扩容,直接返回 
  24.     if (avail >= addlen) return s; 
  25.  
  26. //调用sds.h里面的sdslen,即取可用长度 
  27.     len = sdslen(s); 
  28.  
  29.     sh = (char*)s-sdsHdrSize(oldtype); 
  30.     //len加上要添加的大小 
  31.     newlen = (len+addlen); 
  32.  
  33.     //#define SDS_MAX_PREALLOC (1024*1024) 
  34.     //当新长度小于 1024*1024,直接扩容两倍 
  35.     if (newlen < SDS_MAX_PREALLOC) 
  36.         newlen *= 2; 
  37.     else //当新长度大于 1024*1024,加2014*1024 
  38.         newlen += SDS_MAX_PREALLOC; 
  39.  
  40. //根据长度计算新的类型 
  41.     type = sdsReqType(newlen); 
  42.  
  43.     /* Don't use type 5: the user is appending to the string and type 5 is 
  44.      * not able to remember empty space, so sdsMakeRoomFor() must be called 
  45.      * at every appending operation. */ 
  46.     if (type == SDS_TYPE_5) type = SDS_TYPE_8; 
  47.  
  48. //获取不同结构体的头部大小 
  49.     hdrlen = sdsHdrSize(type); 
  50.     //如果类型一样,直接使用原地址,长度加上就行 
  51.     if (oldtype==type) { 
  52.         newsh = s_realloc(sh, hdrlen+newlen+1); 
  53.         if (newsh == NULLreturn NULL
  54.         s = (char*)newsh+hdrlen; 
  55.     } else {//如果类型不一样,重新开辟内存,把原来的数据复制过去 
  56.         newsh = s_malloc(hdrlen+newlen+1); 
  57.         if (newsh == NULLreturn NULL
  58.         memcpy((char*)newsh+hdrlen, s, len+1); 
  59.         s_free(sh); 
  60.         s = (char*)newsh+hdrlen; 
  61.         s[-1] = type; 
  62.         sdssetlen(s, len); 
  63.     } 
  64.     //设置新的总长度 
  65.     sdssetalloc(s, newlen); 
  66.     return s; 
  67.  
  68. //计算不同类型的结构体的大小 
  69. static inline int sdsHdrSize(char type) { 
  70.     switch(type&SDS_TYPE_MASK) { 
  71.         case SDS_TYPE_5: 
  72.             return sizeof(struct sdshdr5); 
  73.         case SDS_TYPE_8: 
  74.             return sizeof(struct sdshdr8); 
  75.         case SDS_TYPE_16: 
  76.             return sizeof(struct sdshdr16); 
  77.         case SDS_TYPE_32: 
  78.             return sizeof(struct sdshdr32); 
  79.         case SDS_TYPE_64: 
  80.             return sizeof(struct sdshdr64); 
  81.     } 
  82.     return 0; 

总结

该篇主要讲了Redis的底层实现SDS,包括SDS是什么,与传统的C语言相比的优势,具体的逻辑图,常见的方法(包括创建,删除,扩容等)。同时也知道了Redis的embstr和raw的区别。

如果觉得写得还行,麻烦给个赞??,您的认可才是我写作的动力!

如果觉得有说的不对的地方,欢迎评论指出。

好了,拜拜咯。

 

责任编辑:武晓燕 来源: 学习Java的小姐姐
相关推荐

2019-03-07 15:43:22

Redis数据SDS

2023-01-09 08:42:04

String数据类型

2019-10-29 08:59:16

Redis底层数据

2023-11-12 21:49:10

Redis数据库

2020-07-14 08:53:43

Redis数据存储

2024-01-26 06:42:05

Redis数据结构

2019-06-12 22:51:57

Redis软件开发

2019-04-17 15:35:37

Redis数据库数据结构

2019-09-02 09:48:39

Redis数据结构对象

2021-05-21 08:31:09

数据结构二叉树

2021-06-08 06:01:00

C++数据结构向量和数组

2021-08-29 07:41:48

数据HashMap底层

2024-08-12 16:09:31

2020-10-21 12:45:12

Redis数据结构

2019-09-27 08:53:47

Redis数据C语言

2011-03-31 15:41:51

Cacti数据表结构

2021-08-31 07:36:22

LinkedListAndroid数据结构

2023-10-31 08:51:25

数据结构存储数据

2010-03-17 17:19:29

Python数据结构

2019-09-18 08:31:47

数据结构设计
点赞
收藏

51CTO技术栈公众号