深挖 Redis 6.0 源码—— SDS

Posted ITPUB技术小栈

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深挖 Redis 6.0 源码—— SDS相关的知识,希望对你有一定的参考价值。


SDS(Simple Dynamic Strings, 简单动态字符串)是 Redis 的一种基本数据结构,主要是用于存储字符串和整数。 这篇文章里,我们就来探讨一下 Redis SDS 这种数据结构的底层实现原理。

学习之前,首先我们要明确,Redis 是一个使用 C 语言编写的键值对存储系统。

前置思考

我们首先考虑一个问题,如何实现一个二进制安全的字符串?

在 C 语言中, 表示字符串结束,如果字符串中本身就包含  字符,那么字符串就会在  处被截断,即非二进制安全;若通过使用一个 len 属性,来判断字符串是否结束,就可以保证读写字符串时不受到  的影响,则是二进制安全。同时 len 属性也能保证在 O(1) 时间内获取字符串的长度。

Redis 3.2 以前的 SDS 实现

在 Redis 3.2 版本以前,SDS 的结构如下:

struct sdshdr { unsigned int len; unsigned int free; char buf[];};

其中,buf 表示数据空间,用于存储字符串;len 表示 buf 中已占用的字节数,也即字符串长度;free 表示 buf 中剩余可用字节数。

字段 len 和 free 各占 4 字节,紧接着存放字符串。

这样做有以下几个好处:

但其实以上的设计是存在一些问题的,对于不同长度的字符串,是否有必要使用 len 和 free 这 2 个 4 字节的变量?4 字节的 len,可表示的字符串长度为 2^32,而在实际应用中,存放于 Redis 中的字符串往往没有这么长,因此,空间的使用上能否进一步压缩?

那么接下来,我们就来看看最新的 Redis 是如何根据字符串的长度,使用不同的数据结构进行存储的。

Redis SDS 最新实现

在 Redis 3.2 版本之后(v3.2 - v6.0),Redis 将 SDS 划分为 5 种类型:

sdshdr5:长度小于 1 字节sdshdr8:长度 1 字节sdshdr16:长度 2 字节sdshdr32:长度 4 字节sdshdr64:长度 8 字节

Redis 增加了一个 flags 字段来标识类型,用一个字节(8 位)来存储。

其中:前 3 位表示字符串的类型;剩余 5 位,可以用来存储长度小于 32 的短字符串。

struct __attribute__ ((__packed__)) sdshdr5 { unsigned char flags; /* 前3位存储类型,后5位存储长度 */ char buf[]; /* 动态数组,存放字符串 */};

而对于长度大于 31 的字符串,仅仅靠 flags 的后 5 位来存储长度明显是不够的,需要用另外的变量来存储。sdshdr8、sdshdr16、sdshdr32、sdshdr64 的数据结构定义如下,其中 len 表示已使用的长度,alloc 表示总长度,buf 存储实际内容,而 flags 的前 3 位依然存储类型,后 5 位则预留。

struct __attribute__ ((__packed__)) sdshdr8 { uint8_t len; /* 已使用长度,1字节 */ uint8_t alloc; /* 总长度,1字节 */ unsigned char flags; /* 前3位存储类型,后5位预留 */ char buf[];};struct __attribute__ ((__packed__)) sdshdr16 { uint16_t len; /* 已使用长度,2字节 */ uint16_t alloc; /* 总长度,2字节 */ unsigned char flags; /* 前3位存储类型,后5位预留 */ char buf[];};struct __attribute__ ((__packed__)) sdshdr32 { uint32_t len; /* 已使用长度,4字节 */ uint32_t alloc; /* 总长度,4字节 */ unsigned char flags; /* 前3位存储类型,后5位预留 */ char buf[];};struct __attribute__ ((__packed__)) sdshdr64 { uint64_t len; /* 已使用长度,8字节 */ uint64_t alloc; /* 总长度,8字节 */ unsigned char flags; /* 前3位存储类型,后5位预留 */ char buf[];};

以上,Redis 根据字符串长度的不同,选择对应的数据结构进行存储。接下来,我们就来看看 Redis 字符串的相关 API 实现。

SDS API 实现

1. 创建字符串

sds sdsnewlen(const void *init, size_t initlen) { void *sh; sds s; // 根据字符串长度计算相应类型 char type = sdsReqType(initlen); // 如果创建的是""字符串,强转为SDS_TYPE_8 if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8; // 根据类型计算头部所需长度(头部包含 len、alloc、flags) int hdrlen = sdsHdrSize(type); // 指向flags的指针 unsigned char *fp; // 创建字符串,+1是因为 `` 结束符 sh = s_malloc(hdrlen+initlen+1); if (sh == NULL) return NULL; if (init==SDS_NOINIT) init = NULL; else if (!init) memset(sh, 0, hdrlen+initlen+1); // s指向buf s = (char*)sh+hdrlen; // s减1得到flags fp = ((unsigned char*)s)-1; ... // 在s末尾添加结束符 s[initlen] = ''; // 返回指向buf的指针s return s;}

创建 SDS 的大致流程是这样的:首先根据字符串长度计算得到 type,根据 type 计算头部所需长度,然后动态分配内存空间。

注意:

1.创建空字符串时,SDS_TYPE_5 被强制转换为 SDS_TYPE_8(原因是创建空字符串后,内容可能会频繁更新而引发扩容操作,故直接创建为 sdshdr8)2.长度计算有 +1 操作,因为结束符  会占用一个长度的空间。3.返回的是指向 buf 的指针 s。

2. 清空字符串

SDS 提供了两种清空字符串的方法。

void sdsfree(sds s) { if (s == NULL) return; // s减去头部的大小得到结构体的地址 s_free((char*)s-sdsHdrSize(s[-1]));}

另一种是通过重置 len 属性值而达到清空字符串的目的,本质上 buf 并没有被真正清除,新的数据会直接覆盖 buf 中原有的数据,无需申请新的内存空间。

void sdsclear(sds s) { // 将len属性置为0 sdssetlen(s, 0); s[0] = '';}

3. 拼接字符串

SDS 拼接字符串的实现如下:

sds sdscatsds(sds s, const sds t) { return sdscatlen(s, t, sdslen(t));}

可以看到 sdscatsds 内部调用的是 sdscatlen

而 sdscatlen 内部的实现相对复杂一些,由于拼接字符串可能涉及 SDS 的扩容,因此 sdscatlen 内部调用 sdsMakeRoomFor 对拼接的字符串做检查:若无需扩容,直接返回 s;若需要扩容,则返回扩容好的新字符串 s。

sds sdscatlen(sds s, const void *t, size_t len) { // 计算当前字符串长度 size_t curlen = sdslen(s); // 确保s的剩余空间足以拼接上t s = sdsMakeRoomFor(s,len); if (s == NULL) return NULL; // 拼接s、t memcpy(s+curlen, t, len); // 更新s的len属性 sdssetlen(s, curlen+len); // s末尾添加结束符 s[curlen+len] = ''; return s;}

SDS 的扩容策略是这样的:

1.若 SDS 中剩余空闲长度 avail 大于或等于新增内容的长度 addlen,无需扩容。2.若 SDS 中剩余空闲长度 avail 小于或等于 addlen,则分情况讨论:新增后总长度 len+addlen < 1MB 的,按新长度的 2 倍扩容;新增后总长度 len+addlen >= 1MB 的,按新长度加上 1MB 扩容。

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; /* 剩余长度>=新增字符串长度,直接返回 */ if (avail >= addlen) return s; // 计算当前字符串长度len len = sdslen(s); sh = (char*)s-sdsHdrSize(oldtype); // 计算新长度 newlen = (len+addlen); // 新长度<1MB,按新长度的2倍扩容 if (newlen < SDS_MAX_PREALLOC) newlen *= 2; // 否则按新长度+1MB扩容 else newlen += SDS_MAX_PREALLOC; // 计算新长度所属类型 type = sdsReqType(newlen); /* type5不支持扩容,强转为type8 */ if (type == SDS_TYPE_5) type = SDS_TYPE_8; hdrlen = sdsHdrSize(type); if (oldtype==type) { // 类型没变,直接通过realloc扩大动态数组即可。 newsh = s_realloc(sh, hdrlen+newlen+1); if (newsh == NULL) return NULL; s = (char*)newsh+hdrlen; } else { // 类型改变了,则说明头部长度也发生了变化,不进行realloc操作,而是直接重新开辟内存 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; // 为flags赋值 s[-1] = type; // 为len属性赋值 sdssetlen(s, len); } // 为alloc属性赋值 sdssetalloc(s, newlen); return s;}

总结

1.SDS 返回的是指向 buf 的指针,兼容了 C 语言操作字符串的函数,读取内容时,通过 len 属性来限制读取的长度,不受  影响,从而保证二进制安全;2.Redis 根据字符串长度的不同,定义了多种数据结构,包括:sdshdr5/sdshdr8/sdshdr16/sdshdr32/sdshdr64。3.SDS 在设计字符串修改出会调用 sdsMakeRoomFor 函数进行检查,根据不同情况进行扩容。

全文完!


深挖 Redis 6.0 源码—— SDS

2020年10月22日〜24日,由IT168所属ITPUB企业社区平台主办的第十二届中国系统架构师大会(SACC2020)将在云端进行网络直播。自2009年以来,SACC架构师大会已成功举办了十一届,云集了国内CTO,研发总监,高级系统架构师,开发工程师和IT经理等技术人群,与会规模超千人。过去为期3天的议程,涉及20+专场,近120个主题,完整装到发,奋起逆袭的SACC2020,期待您的报名参与,共襄盛举!




扫码查看更多


喜欢就点个在看再走吧

以上是关于深挖 Redis 6.0 源码—— SDS的主要内容,如果未能解决你的问题,请参考以下文章

Redis源码分析01——简单动态字符串(sds)

Redis源码分析01——简单动态字符串(sds)

Redis源码分析01——简单动态字符串(sds)

Redis源码分析01——简单动态字符串(sds)

Redis源码解读——sds

redis源码分析-sds实现