深挖 Redis 6.0 源码—— SDS
本文由 Doocs 开源社区原创,已收录于“源码猎人”项目:https://github.com/doocs/source-code-hunter
SDS(Simple Dynamic Strings, 简单动态字符串)是 Redis 的一种基本数据结构,主要是用于存储字符串和整数。 这篇文章里,我们就来探讨一下 Redis SDS 这种数据结构的底层实现原理。
学习之前,首先我们要明确,Redis 是一个使用 C 语言编写的键值对存储系统。
前置思考
我们首先考虑一个问题,如何实现一个二进制安全的字符串?
在 C 语言中,\0
表示字符串结束,如果字符串中本身就包含 \0
字符,那么字符串就会在 \0
处被截断,即非二进制安全;若通过使用一个 len 属性,来判断字符串是否结束,就可以保证读写字符串时不受到 \0
的影响,则是二进制安全。同时 len 属性也能保证在 O(1) 时间内获取字符串的长度。
Redis 3.2 以前的 SDS 实现
在 Redis 3.2 版本以前,SDS 的结构如下:
其中,buf 表示数据空间,用于存储字符串;len 表示 buf 中已占用的字节数,也即字符串长度;free 表示 buf 中剩余可用字节数。
字段 len 和 free 各占 4 字节,紧接着存放字符串。
这样做有以下几个好处:
用单独的变量 len 和 free,可以方便地获取字符串长度和剩余空间;
内容存储在动态数组 buf 中,SDS 对上层暴露的指针指向 buf,而不是指向结构体 SDS。因此,上层可以像读取 C 字符串一样读取 SDS 的内容,兼容 C 语言处理字符串的各种函数,同时也能通过 buf 地址的偏移,方便地获取其他变量;
读写字符串不依赖于
\0
,保证二进制安全。
但其实以上的设计是存在一些问题的,对于不同长度的字符串,是否有必要使用 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 的短字符串。
而对于长度大于 31 的字符串,仅仅靠 flags 的后 5 位来存储长度明显是不够的,需要用另外的变量来存储。sdshdr8、sdshdr16、sdshdr32、sdshdr64 的数据结构定义如下,其中 len 表示已使用的长度,alloc 表示总长度,buf 存储实际内容,而 flags 的前 3 位依然存储类型,后 5 位则预留。
注意,一般情况下,结构体会按照所有变量大小的最小公倍数做字节对齐,而用 packed
修饰后,结构体则变为 1 字节对齐。这样做的好处有二:一是节省内存,比如 sdshdr32 可节约 3 个字节;二是 **SDS 返回给上层的是指向 buf 的指针,此时按 1 字节对齐,所以可在创建 SDS 后,通过 (char*)sh+hdrlen
得到 buf 指针地址,也可以通过 buf[-1]
找到 flags**。
以上,Redis 根据字符串长度的不同,选择对应的数据结构进行存储。接下来,我们就来看看 Redis 字符串的相关 API 实现。
SDS API 实现
1. 创建字符串
创建 SDS 的大致流程是这样的:首先根据字符串长度计算得到 type,根据 type 计算头部所需长度,然后动态分配内存空间。
注意:
创建空字符串时,
SDS_TYPE_5
被强制转换为SDS_TYPE_8
(原因是创建空字符串后,内容可能会频繁更新而引发扩容操作,故直接创建为 sdshdr8)长度计算有
+1
操作,因为结束符\0
会占用一个长度的空间。返回的是指向 buf 的指针 s。
2. 清空字符串
SDS 提供了两种清空字符串的方法。
一种是通过 s 偏移得到结构体的地址,然后调用 s_free
直接释放内存。
另一种是通过重置 len 属性值而达到清空字符串的目的,本质上 buf 并没有被真正清除,新的数据会直接覆盖 buf 中原有的数据,无需申请新的内存空间。
3. 拼接字符串
SDS 拼接字符串的实现如下:
可以看到 sdscatsds
内部调用的是 sdscatlen
。
而 sdscatlen
内部的实现相对复杂一些,由于拼接字符串可能涉及 SDS 的扩容,因此 sdscatlen
内部调用 sdsMakeRoomFor
对拼接的字符串做检查:若无需扩容,直接返回 s;若需要扩容,则返回扩容好的新字符串 s。
SDS 的扩容策略是这样的:
若 SDS 中剩余空闲长度 avail 大于或等于新增内容的长度 addlen,无需扩容。
若 SDS 中剩余空闲长度 avail 小于或等于 addlen,则分情况讨论:新增后总长度
len+addlen < 1MB
的,按新长度的 2 倍扩容;新增后总长度len+addlen >= 1MB
的,按新长度加上1MB
扩容。
总结
SDS 返回的是指向 buf 的指针,兼容了 C 语言操作字符串的函数,读取内容时,通过 len 属性来限制读取的长度,不受
\0
影响,从而保证二进制安全;Redis 根据字符串长度的不同,定义了多种数据结构,包括:sdshdr5/sdshdr8/sdshdr16/sdshdr32/sdshdr64。
SDS 在设计字符串修改出会调用
sdsMakeRoomFor
函数进行检查,根据不同情况进行扩容。
全文完!
希望本文对大家有所帮助。如果感觉本文有帮助,有劳 infoq 朋友们点赞或者分享一下!让更多人收获知识!
我的 GitHub:https://github.com/yanglbme
版权声明: 本文为 InfoQ 作者【yanglbme】的原创文章。
原文链接:【http://xie.infoq.cn/article/f32a441c87b5fb264696b726a】。文章转载请联系作者。
评论