目录

1、SDS概述

2、SDS的定义

2.1 3.2版本和3.0版本的差别

3、SDS数据结构解读

4、SDS重点源码分析

4.1 创建SDS

4.2 SDS拼接

4.3 SDS惰性空间释放

5、总结


1、SDS概述

        redis 没有直接使用c语言的字符串,而是自己定义了一个字符串数据结构:SDS(simple dynamic string)作为默认的字符串。我们设置的所有键值基本都是SDS
        那么为何 redis 要自己定义一个字符串数据结构呢?我们看看SDS的定义以及他和C语言字符串的区别就清楚了。

2、SDS的定义

2.1 3.2版本和3.0版本的差别

       sds在 redis 3.2版本做了较大的变动,这里就简单顺便介绍一下两个版本的差别。在3.2版本以前SDS只有一种数据结构,到了3.2版本以后SDS根据存储的内容会选择不同的数据结构,以到达节省内存的效果!

// 3.0及以前
struct sdshdr {// 记录buf数组中已使用字节数量unsigned int len;// 记录buf数组中未使用的字节数量unsigned int free;// 字节数组,存储字符串char buf[];
};// >=3.2
struct __attribute__ ((__packed__)) sdshdr5 {unsigned char flags; /* 3 lsb of type, and 5 msb of string length */char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {uint8_t len; /* used */uint8_t alloc; /* excluding the header and null terminator */unsigned char flags; /* 3 lsb of type, 5 unused bits */char buf[];
};
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[];
};

可以看到,在3.2以后的版本,redis 的SDS分为了5种数据结构,分别应对不同长度的字符串需求,具体的类型选择如下。

static inline char sdsReqType(size_t string_size) { // 获取类型if (string_size < 1<<5)     // 32return SDS_TYPE_5;if (string_size < 1<<8)     // 256return SDS_TYPE_8;if (string_size < 1<<16)    // 65536 64kreturn SDS_TYPE_16;if (string_size < 1ll<<32)  // 4294967296 4GBreturn SDS_TYPE_32;return SDS_TYPE_64;
}

__attribute__ ((__packed__))是什么呢?首先就要了解编译器内存对齐的优化策略:

struct的分配的内存是内部最大元素的整数倍

如果内存不对齐,cpu的的内存访问速度会大大下降,至于内存对齐的原理这里就不做赘述了,有兴趣可以自行google或百度。而__attribute__ ((__packed__))这个声明就是用来告诉编译器取消内存对齐优化,按照实际的占用字节数进行对齐,效果如下:

printf("%ld\n", sizeof(struct sdshdr8));  // 3
printf("%ld\n", sizeof(struct sdshdr16)); // 5
printf("%ld\n", sizeof(struct sdshdr32)); // 9
printf("%ld\n", sizeof(struct sdshdr64)); // 17

通过加上__attribute__ ((__packed__))声明,sdshdr16节省了1个字节,sdshdr32节省了3个字节,sdshdr64节省了7个字节。 但是内存对齐怎么办呢,不能为了一点内存大大拖慢cpu的寻址效率啊?redis 通过自己在malloc等c语言内存分配函数上封装了一层zmalloc,将内存分配收敛,并解决了内存对齐的问题。在内存分配前有这么一段代码:

if (_n&(sizeof(long)-1)) _n += sizeof(long)-(_n&(sizeof(long)-1)); \    // 确保内存对齐!

这段代码写的比较抽象,简而言之就是先判断当前要分配的_n个内存是否是long类型的整数倍,如果不是就在_n的基础上加上内存大小差值,从而达到了内存对齐的保证。

3、SDS数据结构解读

扯了一大堆有点偏题了,回到正题,以 redis 3.2.9的sdshdr8为例来解读一下这个数据结构:

  • len记录当前字节数组的长度(不包括\0),使得获取字符串长度的时间复杂度由O(N)变为了O(1)
  • alloc记录了当前字节数组总共分配的内存大小(不包括\0
  • flags记录了当前字节数组的属性、用来标识到底是sdshdr8还是sdshdr16
  • buf保存了字符串真正的值以及末尾的一个\0

整个SDS的内存是连续的,统一开辟的。为何要统一开辟呢?因为在大多数操作中,buf内的字符串实体才是操作对象。如果统一开辟内存就能通过buf头指针进行寻址,拿到整个struct的指针,而且通过buf的头指针减一直接就能获取flags的值,骚操作代码如下!

// flags值的定义
#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获取头指针
#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));
#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))// 通过buf的-1下标拿到flags值
unsigned char flags = s[-1];

至于buf末尾的\0是为了复用string.h中部分字符串操作函数。

4、SDS重点源码分析

4.1 创建SDS

这块比较直白易懂,直接show源码:

sds sdsnewlen(const void *init, size_t initlen) {   // 创建sdsvoid *sh;sds s;  // 指向字符串头指针char type = sdsReqType(initlen);if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;  // 如果是空字符串直接使用SDS_TYPE_8,方便后续拼接int hdrlen = sdsHdrSize(type);unsigned char *fp; /* flags pointer. */sh = s_malloc(hdrlen+initlen+1);    // 分配空间大小为 sdshdr大小+字符串长度+1if (!init)memset(sh, 0, hdrlen+initlen+1);    // 初始化内存空间if (sh == NULL) return NULL;s = (char*)sh+hdrlen;fp = ((unsigned char*)s)-1; // 获取flags指针switch(type) {case SDS_TYPE_5: {*fp = type | (initlen << SDS_TYPE_BITS);    // sdshdr5的前5位保存长度,后3位保存typebreak;}case SDS_TYPE_8: {SDS_HDR_VAR(8,s);       // 获取sdshdr指针sh->len = initlen;      // 设置lensh->alloc = initlen;    // 设置alloc*fp = type; // 设置typebreak;}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';  // 字符数组最后一位设为\0return s;
}

由于sdshdr5的只用来存储长度为32字节以下的字符数组,因此flags的5个bit就能满足长度记录,加上type所需的3bit,刚好为8bit一个字节,因此sdshdr5不需要单独的len记录长度,并且只有32个字节的存储空间,动态的变更内存余地较小,所以 redis 直接不存储alloc,当sdshdr5需要扩展时会直接变更成更大的SDS数据结构。
       除此之外,SDS都会多分配1个字节用来保存'\0'

4.2 SDS拼接

SDS的拼接函数sdscatlen和C语言的strcat有着很大的出入,直接上代码:

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);   // 设置sds->lens[curlen+len] = '\0';       // 在buf的末尾追加一个\0return s;
}

可以看到除了调用了一下sdsMakeRoomFor以外,就是正常的内存拷贝和设置使用长度等等,那么就来看看sdsMakeRoomFor好了:

sds sdsMakeRoomFor(sds s, size_t addlen) {  // 确保sds字符串在拼接时有足够的空间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);    // 获取字符串长度 O(1)sh = (char*)s-sdsHdrSize(oldtype);  // 获取当前sds指针newlen = (len+addlen);  // 分配策略 SDS_MAX_PREALLOC=1024*1024=1Mif (newlen < SDS_MAX_PREALLOC)newlen *= 2;    // 如果拼接后的字符串小于1M,就分配两倍的内存elsenewlen += SDS_MAX_PREALLOC; // 如果拼接后的字符串大于1M,就分配多分配1M的内存type = sdsReqType(newlen);  // 获取新字符串的sds类型if (type == SDS_TYPE_5) type = SDS_TYPE_8;  // 如果type为SDS_TYPE_5直接优化成SDS_TYPE_8hdrlen = sdsHdrSize(type);if (oldtype==type) {newsh = s_realloc(sh, hdrlen+newlen+1); // 类型没变在原有基础上reallocif (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);  // 类型发生变化需要重新mallocif (newsh == NULL) return NULL;memcpy((char*)newsh+hdrlen, s, len+1);  // 将老字符串拷贝到新的内存快中s_free(sh); // 释放老的sds内存s = (char*)newsh+hdrlen;s[-1] = type;   // 设置sds->flagssdssetlen(s, len);  // 设置sds->len}sdssetalloc(s, newlen); // 设置sds->allocreturn s;
}

该方法是用来确保SDS字符串在拼接时有足够的空间,如果空间不够就会重新分配内存,但是分配内存并不是只分配当下要用的内存,而是采用了冗余的预分配内存,通过源码可以看到内存的预分配策略主要有两种:

  1. 拼接后的字符串长度不超过1M,分配两倍的内存
  2. 拼接够的字符串长度超过1M,多分配1M的内存

通过这两种策略,在字符串拼接时会预分配一部分内存,下次拼接的时候就可能不再需要进行内存分配了,将原本N次字符串拼接需要N次内存重新分配的次数优化到最多需要N次,是典型的空间换时间的做法。
当然,如果新的字符串长度超过了原有字符串类型的限定那么还会涉及到一个重新生成sdshdr的过程。

还有一个细节需要注意,由于sdshrd5并不存储alloc值,因此无法获取sdshrd5的可用大小,如果继续采用sdshrd5进行存储,在之后的拼接过程中每次都还是要进行内存重分配。因此在发生拼接行为时,sdshrd5会被直接优化成sdshrd8

4.3 SDS惰性空间释放

SDS的字符串缩短操作中,多余出来的空间并不会直接释放,而是保留这部分空间,待以后再用。以sdstrim为例:

sds sdstrim(sds s, const char *cset) {  // sds trim操作char *start, *end, *sp, *ep;size_t len;sp = start = s;ep = end = s+sdslen(s)-1;while(sp <= end && strchr(cset, *sp)) sp++; // 从头遍历while(ep > sp && strchr(cset, *ep)) ep--;   // 从尾部遍历len = (sp > ep) ? 0 : ((ep-sp)+1);if (s != sp) memmove(s, sp, len);   // 内存拷贝s[len] = '\0';sdssetlen(s,len);   // 重新设置sds->lenreturn s;
}

A. :HelloWroldA.:进行sdstrim(s,"Aa. :");后,如下图所示:

可以看到内存空间并没有被释放,甚至空闲的空间都没有被置空。由于SDS是通过len值标识字符串长度,因此SDS完全不需要受限于c语言字符串的那一套\0结尾的字符串形式。在后续需要拼接扩展时,这部分空间也能够再次被利用起来,降低了内存重新分配的概率。

当然,SDS也提供了真正的释放空间的方法,以供真正需要释放空闲内存时使用。

5、总结

  1. redis 3.2之后,针对不同长度的字符串引入了不同的SDS数据结构,并且强制内存对齐1,将内存对齐交给统一的内存分配函数,从而达到节省内存的目的
  2. SDS的字符串长度通过sds->len来控制,不受限于C语言字符串\0,可以存储二进制数据,并且将获取字符串长度的时间复杂度降到了O(1)
  3. SDS的头和buf字节数组的内存是连续的,可以通过寻址方式获取SDS的指针以及flags
  4. SDS的拼接扩展有一个内存预分配策略,用空间减少每次拼接的内存重分配可能性
  5. SDS的缩短并不会真正释放掉对应空闲空间
  6. SDS分配内存都会多分配1个字节用来在buf的末尾追加一个\0,在部分场景下可以和C语言字符串保证同样的行为甚至复用部分string.h的函数

其他的一些细节或者操作函数这里就不做详解了,如果感兴趣可以自行刷一波源码,SDS相关的源码在sds.hsds.c文件中。

redis源码剖析(1):基础数据结构SDS相关推荐

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

    目录 1.dict概述 2.字典的定义 3.哈希算法 4.字典的初始化及新增键值对 4.1 字典初始化 4.2 新增键值对 5.rehash(重新散列)操作 5.1 rehash操作方式 5.2 re ...

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

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

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

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

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

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

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

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

  6. Redis源码剖析之内存淘汰策略(Evict)

    文章目录 何为Evict 如何Evict Redis中的Evict策略 源码剖析 LRU具体实现 LFU具体实现 LFU计数器增长 LFU计数器衰减 evict执行过程 evict何时执行 evict ...

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

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

  8. Redis源码剖析之GEO——Redis是如何高效检索地理位置的?

    Redis GEO 用做存储地理位置信息,并对存储的信息进行操作.通过geo相关的命令,可以很容易在redis中存储和使用经纬度坐标信息.Redis中提供的Geo命令有如下几个: geoadd:添加经 ...

  9. Redis源码分析:基础概念介绍与启动概述

    Redis源码分析 基于Redis-5.0.4版本,进行基础的源码分析,主要就是分析一些平常使用过程中的内容.仅作为相关内容的学习记录,有关Redis源码学习阅读比较广泛的便是<Redis设计与 ...

最新文章

  1. python制作好看的界面_【一点资讯】Python界面如何漂亮的展示树形结构,PyQt5控件之QTreeWidget详解 www.yidianzixun.com...
  2. python mysql 基于 sqlalvhrmy_Python基于DB-API操作MySQL数据库过程解析
  3. linux下的malloc申请的是虚拟还是物理的,关于malloc申请内存
  4. java int在那个包_int intValue()
  5. 基于linux的应用开发视频教程,基于LINUX+ARM的视频系统的应用和开发
  6. django03配置相关
  7. Windows 10 KB3124200补丁无法安装的临时解决方案
  8. 关于excel数据透视表的数据填充
  9. mongodb 什么比mysql快_MongoDB为什么比mysql效率高
  10. 安卓实现仿系统设置界面
  11. GEE实战 | 各种指数分析(植被、水体、建筑)
  12. ECharts 源码解读 二
  13. 早上喝水较健康 喝法有学问!
  14. Java学习基础语法
  15. 机器学习入门三剑客之Matplotlib
  16. 华为mate50鸿蒙,华为Mate50概念图:棱形摄像头+鸿蒙OS,依靠备胎计划还能否翻身...
  17. 移民新加坡要投资一个亿,如何无痛移民:雇主担保移民推荐给你
  18. JZOJ5462. 【NOIP2017提高A组冲刺11.8】好文章
  19. 学计算机广东2B大学,广东2b大学排名2018及名单
  20. MD制作服装导入DAZ

热门文章

  1. JavaWeb-10(会话技术之sessionamp;JSP)
  2. java委_java双亲委派机制及作用
  3. C++语言基础 例程 派生类的声明与构成
  4. 安装ISE.14.7后仿真时遇到的问题与解决
  5. c swap方法在哪个库里面_覆膜条件下土壤水热动态与玉米种子生长的SWAP修正模型...
  6. blockquote 和 q 标签 css样式
  7. 电大计算机考试将网页保存到桌面,电大计算机网考小抄.pdf
  8. python给定dna等分成两个序列_分析DNA序列中的串联重复序列
  9. 关于用C#编写ActiveX控件4(转)
  10. oracle批量新增字段工具,mybatis 中oracle 批量新增三种方法