前言

现在面试可太卷了,Redis基本是必问的知识点,为了在秋招中卷过其他人(虽然我未必参加秋招),本菜鸡决定从源码层面再次学习Redis,不过鉴于本菜鸡水平有限,且没有c语言基础,本文不会对源码过于深究,达到能在面试中能吹一波的水平即可。本文以黄建宏《Redis设计与实现》为参考书籍,源码选择黄建宏老师提供的带中文注释的redis3.0源码,github地址:https://github.com/huangz1990/redis-3.0-annotated。虽然redis版本较低,但是对于新手来说足够了,当然,在部分地方可能会用到高版本redis。

源码阅读顺序

鉴于面试中redis的数据结构问的最多,而且redis数据结构的实现在大部分算法书上都可能了解到,而且相对于其他部分阅读难度也比较低,所以我选择从基本的数据结构开始阅读。

数据结构与对象

首先我们要先找到各个数据结构的实现文件:

  • sds.h和sds.c :Redis 的动态字符串实现。
  • adlist.h和adlist.c :Redis的双端链表实现。
  • dict.h和dict.c :Redis的字典实现。
  • redis.h 中的 zskiplist 结构和 zskiplistNode 结构, 以及 t_zset.c 中所有以 zsl 开头的函数, 比如 zslCreate 、 zslInsert 、 zslDeleteNode ,等等 : Redis的跳跃表实现。
  • hyperloglog.c 中的 hllhdr 结构, 以及所有以 hll 开头的函数 :Redis 的 HyperLogLog 实现。

在3.0中还没有geohash等数据结构,但是这些数据结构在面试中不常问到,这些如果有机会再探究。

简单动态字符串SDS

Redis没有直接使用C语言传统的字符串,而是自己构建了一种名为简单动态字符串(simple dynamic string)的抽象类型,并将其作为Redis的默认字符串表示。Redis只会使用C语言字符串用作一些无需修改的地方,比如打印日志,当Redis需要的是一个可以被修改的字符串时,redis就会用SDS

来标识字符串值,比如string类型的键值对底层都是由SDS实现的,除此之外SDS还被用作缓冲区(buffer,这里一提,本菜鸡在面字节和腾讯时都问到了buffer和cache,感觉是很重要的知识)。

SDS的定义

直接上SDS的结构体定义:

struct sdshdr {// buf 中已占用空间的长度int len;// buf 中剩余可用空间的长度int free;// 数据空间char buf[];
};

SDS遵循C字符串以“\0”结尾的惯例,保存空字符的1字节空间不计算在SDS的len属性里面,并且为空字符分配额外的1字节空间。遵循空字符结尾使得SDS可以直接重用一部分C字符串函数库里的函数。直接来看SDS初始化方法:

sds sdsnewlen(const void *init, size_t initlen) {struct sdshdr *sh;// 根据是否有初始化内容,选择适当的内存分配方式// T = O(N)if (init) {// zmalloc 不初始化所分配的内存sh = zmalloc(sizeof(struct sdshdr)+initlen+1);} else {// zcalloc 将分配的内存全部初始化为 0sh = zcalloc(sizeof(struct sdshdr)+initlen+1);}// 内存分配失败,返回if (sh == NULL) return NULL;// 设置初始化长度sh->len = initlen;// 新 sds 不预留任何空间sh->free = 0;// 如果有指定初始化内容,将它们复制到 sdshdr 的 buf 中// T = O(N)if (initlen && init)memcpy(sh->buf, init, initlen);// 以 \0 结尾sh->buf[initlen] = '\0';// 返回 buf 部分,而不是整个 sdshdrreturn (char*)sh->buf;
}

该方法根据给定的初始化字符串init和字符串长度initlen创建一个新的sds,其中init参数为初始化字符串的指针,init为初始化字符串的长度,从代码可以看出,当sds创建成功时返回sdshdr的sds,创建失败则返回null。

SDS的优势

常数复杂度获取字符串的长度

因为C语言字符串并不记录自身的长度信息,所以为了获取一个C字符串的长度,程序必须遍历整个字符串,对遇到的每个字符进行计数,直到遇到代表字符串结尾的空字符串为止,这个操作的时间复杂度时O(N)。和C语言字符串不同,因为SDS在len属性中记录了SDS的长度,所以获取一个SDS长度的复杂度仅为O(1),同时设置和更新SDS长度的工作是由SDS的API在执行时自动完成的,使用SDS无须进行任何手动修改长度的工作。

杜绝缓冲区溢出

除了获取字符串长度的复杂度高之外,C语言字符串不记录自身长度带来的另一个问题时容易造成缓冲区溢出。比如在C语言中,如果要将一个字符串s2拼接到字符串s1尾部,如过没有为s1分配足够多的内存,那么就会产生缓冲区溢出。而SDS的空间分配策略完全杜绝了发生缓冲区溢出的可能性:当SDS API需要对SDS进行修改时,API会先检查SDS的空间是否满足修改所需的要求,如果不满足的话,API会自动将SDS的空间扩展至执行修改所需要的大小,然后才执行实际的修改操作。

这里我们以sdscat函数(将指定的字符串t追加到sds尾部)的源码为例:

sds sdscat(sds s, const char *t) {return sdscatlen(s, t, strlen(t));
}

在进入sdscat函数:

sds sdscatlen(sds s, const void *t, size_t len) {struct sdshdr *sh;// 原有字符串长度size_t curlen = sdslen(s);// 扩展 sds 空间// T = O(N)s = sdsMakeRoomFor(s,len);// 内存不足?直接返回if (s == NULL) return NULL;// 复制 t 中的内容到字符串后部// T = O(N)sh = (void*) (s-(sizeof(struct sdshdr)));memcpy(s+curlen, t, len);// 更新属性sh->len = curlen+len;sh->free = sh->free-len;// 添加新结尾符号s[curlen+len] = '\0';// 返回新 sdsreturn s;
}

这里我们可以看到在将t字符串追加到sds尾部时,首先会通过sdsMakeRoomFor函数扩展sds空间,保证sds至少有t字符串长度+1的空闲内存,然后再将t字符串追加到sds尾部,所以不会出现缓冲区溢出的问题。

减少修改字符串时带来的内存重分配次数

为了避免C语言字符串每次加长到需要重新扩展分配内存的缺陷,SDS中buf数组的长度不一定就是字符数量加一,数组里面可以包含未使用的字节,而这些字节的数量就由SDS的free属性记录。

通过未使用空间,SDS实现了空间预分配和惰性空间释放两种优化策略。

空间预分配

空间预分配用于优化SDS的字符串增长操作,当SDS的API对一个SDS进行修改,并且需要对SDS进行空间扩展时,程序不仅会对SDS分配修改所必须要的空间,还会为SDS分配额外的未使用空间。

这里我们直接上源码:

sds sdsMakeRoomFor(sds s, size_t addlen) {struct sdshdr *sh, *newsh;// 获取 s 目前的空余空间长度size_t free = sdsavail(s);size_t len, newlen;// s 目前的空余空间已经足够,无须再进行扩展,直接返回if (free >= addlen) return s;// 获取 s 目前已占用空间的长度len = sdslen(s);sh = (void*) (s-(sizeof(struct sdshdr)));// s 最少需要的长度newlen = (len+addlen);// 根据新长度,为 s 分配新空间所需的大小if (newlen < SDS_MAX_PREALLOC)// 如果新长度小于 SDS_MAX_PREALLOC // 那么为它分配两倍于所需长度的空间newlen *= 2;else// 否则,分配长度为目前长度加上 SDS_MAX_PREALLOCnewlen += SDS_MAX_PREALLOC;// T = O(N)newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);// 内存不足,分配失败,返回if (newsh == NULL) return NULL;// 更新 sds 的空余长度newsh->free = newlen - len;// 返回 sdsreturn newsh->buf;
}

我们可以看到,在扩展内存时,首先要判断空余内存是否大于要扩展的内存,如果大于,则直接返回sds。否则,让newlen = 已使用内存 + 要扩展的内存,然后判断newlen是否大于1M,如果小于1M,那么newlen扩大一倍,要过大于1M,newlen增加1M,最后给sds分配newlen+1byte的内存。

为了便于理解,这里举个例子,如果原有sds已用10字节,要扩展5字节,那么会分配另外15字节的空闲空间,即扩展后sds的buf数组的实际长度为(10 + 5)*2 + 1 = 31字节。如果原有sds已用10M,要分配5M,那么会额外分配另外1M的空闲空间,那么扩展后sds的buf数组的实际长度为10M + 5M + 1byte。

通过这种空间预分配策略,Redis可以减少连续执行字符串增长操作所需的内存重分配次数。

惰性空间释放

惰性空间释放用于优化SDS的字符串缩短操作:当SDS的API需要缩短SDS保存的字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用free属性将这些字节的数量记录起来。

这里以sdstrim函数的源码为例,该函数用于修剪字符串中cset字符,直接上源码:

sds sdstrim(sds s, const char *cset) {struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));char *start, *end, *sp, *ep;size_t len;// 设置和记录指针sp = start = s;ep = end = s+sdslen(s)-1;// 修剪, T = O(N^2)while(sp <= end && strchr(cset, *sp)) sp++;while(ep > start && strchr(cset, *ep)) ep--;// 计算 trim 完毕之后剩余的字符串长度len = (sp > ep) ? 0 : ((ep-sp)+1);// 如果有需要,前移字符串内容// T = O(N)if (sh->buf != sp) memmove(sh->buf, sp, len);// 添加终结符sh->buf[len] = '\0';// 更新属性sh->free = sh->free+(sh->len-len);sh->len = len;// 返回修剪后的 sdsreturn s;
}

我们可以看到,在该方法中并没有释放sds多余的内存,而是将buf的len位置设为空字符,free设为原有的len减去修剪后的len,len设为修剪后的len。

二进制安全

C语言字符串中的字符必须符合某种编码,并且除了字符串的末尾之外,字符串里面不能包含空字符串,这就限制了C语言字符串不能保存图片、音频、视频等二进制数据。

而SDS的API都会以处理二进制的方式来处理SDS存放在buf数组里的数据,程序不会对其中的数据做任何限制、过滤,数据在写入时是什么样的,读取出来就是什么样的。

兼容部分C字符串

虽然SDS的API都是二进制安全的,但它们一样遵循C字符串以空字符结尾的惯例,这是为了让那些保存文本数据的SDS可以重用一部分<string.h>库定义的函数,比如像strcasecmp(用于比较两个字符串)等函数SDS都可直接使用。

sds.c中其他API不再一一查看,阅读源码的目的主要是为了了解设(应)计(付)思(面)想(试),而不是深究实现细节。

Redis源码初探(1)简单动态字符串SDS相关推荐

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

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

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

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

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

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

  4. Redis之简单动态字符串sds

    转载:https://segmentfault.com/a/1190000012262739 redis在处理字符串的时候没有直接使用以'\0'结尾的C语言字符串,而是封装了一下C语言字符串并命名为s ...

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

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

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

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

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

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

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

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

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

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

最新文章

  1. Git Bash修改默认路径
  2. javascript,令人着迷了!
  3. C/C++ memmove与memcpy的区别及实现
  4. cjson使用_LD3320语音识别模块:LDV7模块使用详解
  5. matlab中K=[K,temp]含义
  6. 高效查询ECS可用资源的实践
  7. 【Storm总结-6】Twitter Storm: DRPC简介
  8. Create a Search Scope for a Sharepoint 2010 List or Library
  9. 【HiFlow】新型零代码自动化助手
  10. html鼠标点击后变换样式,css鼠标样式(css鼠标点击切换样式)
  11. markdown快捷键大全
  12. 计算机word的关闭怎么办,为什么我的计算机word文档打开和关闭缓慢
  13. oracle数据库查看防火墙,Oracle数据库防火墙简介
  14. Java上帝类(Object类)源码总结(1)
  15. 苹果手机照片流使用方法(iphone我的照片流在哪)
  16. 实验2:天气查询小程序
  17. 灵备CDM的技术及原理
  18. 传统激光条纹中心提取算法研究现状
  19. 编辑为什么建议转投_SCI编辑建议转投容易录用吗
  20. Android视频滤镜添加硬解码方案

热门文章

  1. Pytorch autograd.grad与autograd.backward详解
  2. 茅指数成分股投资收益可视化
  3. i-Shanghai无法跳转登陆页面/登陆页面打不开的解决方法
  4. 企业在实施采购管理时需要注意哪些问题?
  5. jspm彩虹滑板专卖网店系统毕业设计(附源码、运行环境)
  6. FFmpeg 快速上手:命令行详解、工具、教程、电子书
  7. Unity 基础 之 xml 使用 Office Excel 轻松编辑保存 xml 数据,并解析读取数据
  8. MathType中/英文版下载地址汇总(适用于Mathtype6.9)
  9. MySQL50题-第6-10题
  10. Valentino Beauty华伦天奴美妆即将登陆中国市场