面试杀手锏:Redis源码之SDS

1.前言

Hello,欢迎大家来到《 Redis 数据结构源码解析系列》,在《Redis为什么这么快?》一文中我说过 Redis 速度快的一个原因就是其简单且高效的数据结构。

本系列文章面向各个阶段的 Coder 们,新手也不用怕。每一篇文章敖丙都将从命令实战入门入手,随后深入源码解析,最后面试题回顾这三个方向上给各位卷王一一介绍。

2.SDS命令实战[初来乍到]

SDS 是 Redis 中最简单的数据结构。Redis 中所有的数据结构都是以唯一的 key 字符串作为名称,根据 key 获取value,差异仅在于 value 的数据结构不同。

SDS 在生产环境中使用非常广泛,比如,我们使用 SDS 做分布式锁;将对象转成 JSON 串作为缓存等。

在 Redis 面试过程中一旦提及相关数据结构 SDS 一定是绕不过去的话题,它很简单(或者说看完此文后很简单),面试官可以不问,但我们不能不懂。

首先我们从命令实战开始切入吧~(老司机直接跳过)

更多命令查看官网:https://redis.io/commands#string

2. 1设置字符串

格式:set 。其中value的值可以为字节串(byte string)、整型和浮点数。

>setnameaobing
OK

2.2 获取字符串

格式:get 。

>getname
"aobing"

2.3 获取字符串长度

格式:strlen

>strlenname
(integer) 6

2.4 获取子串

格式:getrange start end。获取字符串的子串,在Redis2.0之前此命令为substr,现使用getrange。返回位移为start(从0开始)和end之间(都包括,而不是像其他语言中的包头不包尾)的子串。

可以使用负偏移量来提供从字符串末尾开始的偏移量。因此-1表示最后一个字符,-2表示倒数第二个,依此类推。

该函数通过将结果范围限制为字符串的实际长度来处理超出范围的请求(end设置非常大也是到字符串末尾就截止了)。

127.0.0.1:6379>setmykey"This is a string"
OK
127.0.0.1:6379>getrangemykey03
"This"
127.0.0.1:6379>getrangemykey-3-1
"ing"
127.0.0.1:6379>getrangemykey0-1
"This is a string"
127.0.0.1:6379>getrangemykey1010000

2.5 设置子串

格式:setrange offset substr。返回值:修改后字符串的长度。

从value的整个长度开始,从指定的偏移量覆盖key处存储的一部分字符串。如果偏移量大于key处字符串的当前长度,则该字符串将填充零字节以使偏移量适合。

不存在的键被视为空字符串,因此此命令将确保它包含足够大的字符串以能够将值设置为offset。

注意:您可以设置的最大偏移为2^29 – 1(536870911),因为Redis字符串限制为512 MB。如果您需要超出此大小,可以使用多个键。

127.0.0.1:6379>setkey1"hello world"
OK
127.0.0.1:6379>setrangekey16redis
(integer) 11
127.0.0.1:6379>getkey1
"hello redis"
127.0.0.1:6379>setrangekey26redis
(integer) 11
127.0.0.1:6379>getkey2
"\x00\x00\x00\x00\x00\x00redis"

2.6 追加子串

格式:append substr如果key已经存在并且是字符串,则此命令将value在字符串末尾附加。如果key不存在,则会创建它并将其设置为空字符串,因此APPEND在这种特殊情况下 将类似于SET。

127.0.0.1:6379>existskey4
(integer) 0
127.0.0.1:6379>appendkey4hello
(integer) 5
127.0.0.1:6379>appendkey4world
(integer) 10
127.0.0.1:6379>getkey4
"helloworld"

2.7 计数

在使用Redis中我们经常将字符串做为计数器,使用incr命令进行加一。格式:incr 。返回值:key递增后的值。将存储的数字key加1。如果key不存在,则在执行操作之前将其设置为0。如果key包含错误类型的值或包含不能表示为整数的字符串,则返回错误。此操作仅限于64位带符号整数。计数是由范围的,它不能超过Long.Max,不能低于Long.Min。

2.8 过期和删除

字符串可以使用del命令进行删除,也可以使用expire命令设置过期时间,到期自动删除。我们可以使用ttl命令获取字符串的寿命(还有多少时间过期)。

格式:del …返回值:删除key的个数

127.0.0.1:6379>SETkey1"Hello"
"OK"
127.0.0.1:6379>SETkey2"World"
"OK"
127.0.0.1:6379>DELkey1key2key3
(integer) 2

格式:expire time返回值:如果设置了超时返回1。如果key不存在返回0。

如何将设置了过期的字符串设置为永久的呢?

生存时间可以通过使用DEL命令来删除整个 key 来移除,或者被SET和GETSET命令覆写(overwrite),这意味着,如果一个命令只是修改一个带生存时间的 key 的值而不是用一个新的 key 值来代替(replace)它的话,那么生存时间不会被改变。

比如说,对一个 key 执行INCR命令,对一个列表进行LPUSH命令,或者对一个哈希表执行HSET命令,这类操作都不会修改 key 本身的生存时间。

如果使用RENAME对一个 key 进行改名,那么改名后的 key 的生存时间和改名前一样。

RENAME 命令的另一种可能是,尝试将一个带生存时间的 key 改名成另一个带生存时间的 another_key ,这时旧的 another_key (以及它的生存时间)会被删除,然后旧的 key 会改名为 another_key ,因此,新的 another_key 的生存时间也和原本的 key 一样。

使用PERSIST命令可以在不删除 key 的情况下,移除 key 的生存时间,让 key 重新成为一个『持久的』(persistent) key 。

127.0.0.1:6379>expireage100
(integer) 1
127.0.0.1:6379>ttlage
(integer) 97
127.0.0.1:6379>setage20
OK
127.0.0.1:6379>ttlage
(integer) -1
127.0.0.1:6379>expireage100
(integer) 1
127.0.0.1:6379>ttlage
(integer) 98
127.0.0.1:6379>renameageage2
OK
127.0.0.1:6379>ttlage2
(integer) 87
127.0.0.1:6379>expireage100
(integer) 1
127.0.0.1:6379>ttlage
(integer) 96
127.0.0.1:6379>persistage
(integer) 1
127.0.0.1:6379>ttlage
(integer) -1

3.SDS 简介与特性[八股]

Redis 面试中大概率会提及相关的数据结构,SDS 的八股文大部分人倒背如流,可是没有读过源码,知其然不知其所以然,这可万万使不得呀!!

4.SDS 结构模型[老司机]

本次敖丙阅读的Redis源码为最新的 Redis6.2.6 和 Redis3.0.0 版本

相信各位看官在听到 Redis 中的字符串不是简简单单的C语言中的字符串,是 SDS(Simple Dynamic String,简单动态字符串)时以为是造出了啥新类型呢,对此,敖丙想说的是不慌,其实 SDS 内容的源码底层就是typedef char *sds;。

4.1 数据结构

Redis6.x 的 SDS 的数据结构定义与 Redis3.0.0 相差比较大,但是核心思想不变。先从简单版本(Redis3.x)开始吧~

structsdshdr {
//记录buf数组中已使用字节的数量
//等于SDS所保存字符串的长度
unsignedintlen;

//记录buf数组中未使用字节的数量
unsignedintfree;

//char数组,用于保存字符串
charbuf[];
};

如下图所示为字符串”Aobing”在Redis中的存储形式:

  • len 为6,表示这个 SDS 保存了一个长度为5的字符串;
  • free 为0,表示这个 SDS 没有剩余空间;
  • buf 是个char类型的数组,注意末尾保存了一个空字符’\0’。

buf 尾部自动追加一个’\0’字符并不会计算在 SDS 的len中,这是为了遵循 C 字符串以空字符串结尾的惯例,使得 SDS 可以直接使用一部分string.h库中的函数,如strlen

#include <stdio.h>
#include <string.h>

intmain()
{
charbuf[] = {'A','o','b','i','n','g','\0'};
printf("%s\n",buf); //Aobing
printf("%lu\n",strlen(buf)); //6
return0;
}

4.2 苛刻的数据优化

4.2.1 数据结构优化

目前我们似乎得到了一个结构不错的 SDS ,但是我们能否继续进行优化呢?

在 Redis3.x 版本中不同长度的字符串占用的头部是相同的,如果某一字符串很短但是头部却占用了更多的空间,这未免太浪费了。所以我们将 SDS 分为三种级别的字符串:

  • 短字符串(长度小于32),len和free的长度用1字节即可;
  • 长字符串,用2字节或者4字节;
  • 超长字符串,用8字节。

共有五种类型的SDS(长度小于1字节、1字节、2字节、4字节、8字节)

我们可以在 SDS 中新增一个 type 字段来标识类型,但是没必要使用一个 4 字节的int类型去做!可以使用 1 字节的char类型,通过位运算(3位即可标识2^3种类型)来获取类型。

如下所示为短字符串(长度小于32)的优化形式:

低三位存储类型,高5位存储长度,最多能标识的长度为32,所以短字符串的长度必定小于32。

无需free字段了,32-len即为free

敖丙带大家分析了一波,接下来看看Redis6.x中是怎么做的吧!

//注意:sdshdr5从未被使用,Redis中只是访问flags。
struct__attribute__ ((__packed__)) sdshdr5 {
unsignedcharflags; /*低3位存储类型, 高5位存储长度*/
charbuf[];
};
struct__attribute__ ((__packed__)) sdshdr8 {
uint8_tlen; /*已使用*/
uint8_talloc; /*总长度,用1字节存储*/
unsignedcharflags; /*低3位存储类型, 高5位预留*/
charbuf[];
};
struct__attribute__ ((__packed__)) sdshdr16 {
uint16_tlen; /*已使用*/
uint16_talloc; /*总长度,用2字节存储*/
unsignedcharflags; /*低3位存储类型, 高5位预留*/
charbuf[];
};
struct__attribute__ ((__packed__)) sdshdr32 {
uint32_tlen; /*已使用*/
uint32_talloc; /*总长度,用4字节存储*/
unsignedcharflags; /*低3位存储类型, 高5位预留*/
charbuf[];
};
struct__attribute__ ((__packed__)) sdshdr64 {
uint64_tlen; /*已使用*/
uint64_talloc; /*总长度,用8字节存储*/
unsignedcharflags; /*低3位存储类型, 高5位预留*/
charbuf[];
};

数据结构和我们分析的差不多嘛!也是加一个标识字段而已,并且不是int类型,而是1字节的char类型,使用其中的3位表示具体的类型。

同时,Redis 中也声明了5个常量分别表示五种类型的 SDS ,与我们分析的也不谋而合。

#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

4.2.2 uintX_t

对比前后两版代码,不难发现在 Redis6.x 中 int 类型也多出了几种:uint8_t / uint16_t / uint32_t /uint64_t。乍一看以为是新增类型呢,毕竟 C语言里面可没有这些类型呀!

敖丙初见也是满头雾水,毕竟C 语言忘得差不多了。不过我凭借强大的知识储备(不要face ^_^)猜测这可能是一个别名,C语言中有typedef呀!而_t就是其缩写。查看相关源码,果然~~

typedefunsignedcharuint8_t;
typedefunsignedshortuint16_t;
typedefunsignedintuint32_t;
typedefunsignedlonglonguint64_t;

4.2.3 对齐填充

在 Redis6.x 的源码中 SDS 的结构体为struct __attribute__ ((__packed__))与struct有较大的差别,这其实和我们熟知的对齐填充有关。

(1) 举个栗子

考虑如下结构体:

typedefstruct{
charc1;
shorts;
charc2;
inti;
} s;

若此结构体中的成员都是紧凑排列的,假设c1的起始地址为0,则s的地址为1,c2的地址为3,i的地址为4。下面用代码论证一下我们的假设。

#include <stdio.h>

typedefstruct
{
charc1;
shorts;
charc2;
inti;
} s;

intmain()
{
sa;
printf("c1 -> %d, s -> %d, c2 -> %d, i -> %d\n",
(unsignedint)(void*)&a.c1- (unsignedint)(void*)&a,
(unsignedint)(void*)&a.s- (unsignedint)(void*)&a,
(unsignedint)(void*)&a.c2- (unsignedint)(void*)&a,
(unsignedint)(void*)&a.i- (unsignedint)(void*)&a);
return0;
}
//结果为:c1->0, s->2, c2->4, i->8

尴尬了,和假设差的不是一星半点呀!这就是对齐填充搞的鬼,这啥啥啥呀~

(2) 什么是字节对齐

现代计算机中,内存空间按照字节划分,理论上可以从任何起始地址访问任意类型的变量。

但实际中在访问特定类型变量时经常在特定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序一个接一个地存放,这就是对齐。

(3) 对齐原因

为什么需要对齐填充是由于各个硬件平台对存储空间的处理上有很大的不同。

一些平台对某些特定类型的数据只能从某些特定地址开始存取。最常见的是如果不按照适合其平台的要求对数据存放进行对齐,会在存取效率上带来损失。

比如有些平台每次读都是从偶地址开始,如果一个int型(假设为 32位)存放在偶地址开始的地方,那么一个读周期就可以读出,而如果存放在奇地址开始的地方,就可能会需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该int数据,导致在读取效率上下降很多。

(4) 更改对齐方式

注意:我们写程序的时候,不需要考虑对齐问题。编译器会替我们选择适合目标平台的对齐策略。

如果我们一定要手动更改对齐方式,一般可以通过下面的方法来改变缺省的对界条件:

  • 使用伪指令#pragma pack(n):C编译器将按照n个字节对齐;
  • 使用伪指令#pragma pack():取消自定义字节对齐方式。
  • 另外,还有如下的一种方式(GCC特有语法):
  • __attribute((aligned (n))):让所作用的结构成员对齐在n字节自然边界上。如果结构体中有成员的长度大于n,则按照最大成员的长度来对齐。
  • __attribute__ ((packed)):取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐。

将上述示例代码的结构体更改如下(取消对齐),再次执行,可以发现取消对齐后和我们的假设就一致了。

typedefstruct__attribute__ ((__packed__))
{
charc1;
shorts;
charc2;
inti;
} s;
//再次执行:c1->0, s->1, c2->3, i->4

(5) Redis为什么不对齐呢?

综上所述我们知道了对齐填充可以提高 CPU 的数据读取效率,作为 IO 频繁的 Redis 为什么选择不对齐呢?

我们再次回顾 Redis6.x 中的 SDS 结构:

有个细节各位需要知道,即 SDS 的指针并不是指向 SDS 的起始位置(len位置),而是直接指向buf[],使得 SDS 可以直接使用 C 语言string.h库中的某些函数,做到了兼容,十分nice~。

如果不进行对齐填充,那么在获取当前 SDS 的类型时则只需要后退一步即可flagsPointer = ((unsigned char*)s)-1;相反,若进行对齐填充,由于 Padding 的存在,我们在不同的系统中不知道退多少才能获得flags,并且我们也不能将 sds 的指针指向flags,这样就无法兼容 C 语言的函数了,也不知道前进多少才能得到 buf[]。

4.3 SDS 优势

4.3.1 O(1)时间复杂度获取字符串长度

由于C字符串不记录自身的长度,所以为了获取一个字符串的长度程序必须遍历这个字符串,直至遇到’0’为止,整个操作的时间复杂度为O(N)。而我们使用SDS封装字符串则直接获取len属性值即可,时间复杂度为O(1)。

4.3.2 二进制安全

什么是二进制安全?

通俗地讲,C语言中,用’0’表示字符串的结束,如果字符串本身就有’0’字符,字符串就会被截断,即非二进制安全;若通过某种机制,保证读写字符串时不损害其内容,则是二进制安全。

C字符串中的字符除了末尾字符为’\0’外其他字符不能为空字符,否则会被认为是字符串结尾(即使实际上不是)。

这限制了C字符串只能保存文本数据,而不能保存二进制数据。而SDS使用len属性的值判断字符串是否结束,所以不会受’\0’的影响。

4.3.3 杜绝缓冲区溢出

字符串的拼接操作是使用十分频繁的,在C语言开发中使用char *strcat(char *dest,const char *src)方法将src字符串中的内容拼接到dest字符串的末尾。由于C字符串不记录自身的长度,所有strcat方法已经认为用户在执行此函数时已经为dest分配了足够多的内存,足以容纳src字符串中的所有内容,而一旦这个条件不成立就会产生缓冲区溢出,会把其他数据覆盖掉,Dangerous~。

//strcat源码
char*__cdeclstrcat (char*dst, constchar*src)
{
char*cp=dst;

while( *cp )
cp++; /*找到dst的结尾*/

while( *cp++=*src++ ) ; /*无脑将src复制到dst*/

return( dst ); /*返回dst*/
}

如下图所示为一次缓冲区溢出:

与C字符串不同,SDS 的自动扩容机制完全杜绝了发生缓冲区溢出的可能性:

当SDS API需要对SDS进行修改时,API会先检查 SDS 的空间是否满足修改所需的要求,如果不满足,API会自动将SDS的空间扩展至执行修改所需的大小,然后才执行实际的修改操作,所以使用 SDS 既不需要手动修改SDS的空间大小,也不会出现缓冲区溢出问题。

SDS 的sds sdscat(sds s, const char *t)方法在字符串拼接时会进行扩容相关操作。

sdssdscatsds(sdss, constsdst) {
returnsdscatlen(s, t, sdslen(t));
}

/*s: 源字符串
*t: 待拼接字符串
*len: 待拼接字符串长度
*/
sdssdscatlen(sdss, constvoid*t, size_tlen) {
//获取源字符串长度
size_tcurlen=sdslen(s);
//SDS分配空间(自动扩容机制)
s=sdsMakeRoomFor(s,len);
if (s==NULL) returnNULL;
//将目标字符串拷贝至源字符串末尾
memcpy(s+curlen, t, len);
//更新SDS长度
sdssetlen(s, curlen+len);
//追加结束符
s[curlen+len] ='\0';
returns;
}

自动扩容机制——sdsMakeRoomFor方法

strcatlen中调用sdsMakeRoomFor完成字符串的容量检查及扩容操作,重点分析此方法:

/*s: 源字符串
*addlen: 新增长度
*/
sdssdsMakeRoomFor(sdss, size_taddlen) {
void*sh, *newsh;
//sdsavail: s->alloc-s->len, 获取SDS的剩余长度
size_tavail=sdsavail(s);
size_tlen, newlen, reqlen;
//根据flags获取SDS的类型oldtype
chartype, oldtype=s[-1] &SDS_TYPE_MASK;
inthdrlen;
size_tusable;

/*ReturnASAPifthereisenoughspaceleft. */
//剩余空间大于等于新增空间,无需扩容,直接返回源字符串
if (avail>=addlen) returns;
//获取当前长度
len=sdslen(s);
//
sh= (char*)s-sdsHdrSize(oldtype);
//新长度
reqlen=newlen= (len+addlen);
//断言新长度比原长度长,否则终止执行
assert(newlen>len); /*防止数据溢出*/
//SDS_MAX_PREALLOC=1024*1024, 即1MB
if (newlen<SDS_MAX_PREALLOC)
//新增后长度小于1MB,则按新长度的两倍扩容
newlen*=2;
else
//新增后长度大于1MB,则按新长度加上1MB扩容
newlen+=SDS_MAX_PREALLOC;
//重新计算SDS的类型
type=sdsReqType(newlen);

/*Don't use type 5: the user is appending to the string and type 5 is
*notabletorememberemptyspace, sosdsMakeRoomFor() mustbecalled
*ateveryappendingoperation. */
//不使用sdshdr5
if (type==SDS_TYPE_5) type=SDS_TYPE_8;
//获取新的header大小
hdrlen=sdsHdrSize(type);
assert(hdrlen+newlen+1>reqlen); /*Catchsize_toverflow*/
if (oldtype==type) {
//类型没变
//调用s_realloc_usable重新分配可用内存,返回新SDS的头部指针
//usable会被设置为当前分配的大小
newsh=s_realloc_usable(sh, hdrlen+newlen+1, &usable);
if (newsh==NULL) returnNULL; //分配失败直接返回NULL
//获取指向buf的指针
s= (char*)newsh+hdrlen;
} else {
//类型变化导致header的大小也变化,需要向前移动字符串,不能使用realloc
newsh=s_malloc_usable(hdrlen+newlen+1, &usable);
if (newsh==NULL) returnNULL;
//将原字符串copy至新空间中
memcpy((char*)newsh+hdrlen, s, len+1);
//释放原字符串内存
s_free(sh);
s= (char*)newsh+hdrlen;
//更新SDS类型
s[-1] =type;
//设置长度
sdssetlen(s, len);
}
//获取buf总长度(待定)
usable=usable-hdrlen-1;
if (usable>sdsTypeMaxSize(type))
//若可用空间大于当前类型支持的最大长度则截断
usable=sdsTypeMaxSize(type);
//设置buf总长度
sdssetalloc(s, usable);
returns;
}

自动扩容机制总结:

扩容阶段:

若 SDS 中剩余空闲空间 avail 大于新增内容的长度 addlen,则无需扩容;

若 SDS 中剩余空闲空间 avail 小于或等于新增内容的长度 addlen:

  • 若新增后总长度 len+addlen < 1MB,则按新长度的两倍扩容;
  • 若新增后总长度 len+addlen > 1MB,则按新长度加上 1MB 扩容。

内存分配阶段:

根据扩容后的长度选择对应的 SDS 类型:

  • 若类型不变,则只需通过 s_realloc_usable扩大 buf 数组即可;
  • 若类型变化,则需要为整个 SDS 重新分配内存,并将原来的 SDS 内容拷贝至新位置。

自动扩容流程图如下所示:

扩容后的 SDS 不会恰好容纳下新增的字符,而是多分配了一些空间(预分配策略),这减少了修改字符串时带来的内存重分配次数

4.3.4 内存重分配次数优化

(1) 空间预分配策略

因为 SDS 的空间预分配策略, SDS 字符串在增长过程中不会频繁的进行空间分配。

通过这种分配策略,SDS 将连续增长N次字符串所需的内存重分配次数从必定N次降低为最多N次。

(2) 惰性空间释放机制

空间预分配策略用于优化 SDS 增长时频繁进行空间分配,而惰性空间释放机制则用于优化 SDS 字符串缩短时并不立即使用内存重分配来回收缩短后多出来的空间,而仅仅更新 SDS 的len属性,多出来的空间供将来使用。

SDS 中调用sdstrim方法来缩短字符串:

/*sdstrim方法删除字符串首尾中在cset中出现过的字符
*比如:
*s=sdsnew("AA...AA.a.aa.aHelloWorld :::");
*s=sdstrim(s,"Aa. :");
*printf("%s\n", s);
*
*SDS变成了"HelloWorld"
*/
sdssdstrim(sdss, constchar*cset) {
char*start, *end, *sp, *ep;
size_tlen;

sp=start=s;
ep=end=s+sdslen(s)-1;
//strchr()函数用于查找给定字符串中某一个特定字符
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';
//仅仅更新了len
sdssetlen(s,len);
returns;
}

勘误

在《Redis的设计与实现》一书中针对 sdstrim方法的讲解为:删除字符串中 cset 出现的所有字符,而不是首尾。

比如:调用sdstrim(“XYXaYYbcXyY”,”XY”),后移除了所有的’X’和’Y’。这是错误的~

SDS 并没有释放多出来的5字节空间,仅仅将 len 设置成了7,剩余空间为5。如果后续字符串增长时则可以派上用场(可能不需要再分配内存)。

也许各位又会有疑问了,这没真正释放空间,是否会导致内存泄漏呢?

放心,SDS为我们提供了真正释放SDS未使用空间的方法sdsRemoveFreeSpace。

sdssdsRemoveFreeSpace(sdss) {
void*sh, *newsh;
//获取类型
chartype, oldtype=s[-1] &SDS_TYPE_MASK;
//获取header大小
inthdrlen, oldhdrlen=sdsHdrSize(oldtype);
//获取原字符串长度
size_tlen=sdslen(s);
//获取可用长度
size_tavail=sdsavail(s);
//获取指向头部的指针
sh= (char*)s-oldhdrlen;

/*ReturnASAPifthereisnospaceleft. */
if (avail==0) returns;

//查找适合这个字符串长度的最优SDS类型
type=sdsReqType(len);
hdrlen=sdsHdrSize(type);

/*如果类型相同,或者至少仍然需要一个足够大的类型,我们只需reallocbuf即可;
*否则,说明变化很大,则手动重新分配字符串以使用不同的头文件类型。
*/
if (oldtype==type||type>SDS_TYPE_8) {
newsh=s_realloc(sh, oldhdrlen+len+1);
if (newsh==NULL) returnNULL;
s= (char*)newsh+oldhdrlen;
} else {
newsh=s_malloc(hdrlen+len+1);
if (newsh==NULL) returnNULL;
memcpy((char*)newsh+hdrlen, s, len+1);
//释放内存
s_free(sh);
s= (char*)newsh+hdrlen;
s[-1] =type;
sdssetlen(s, len);
}
//重新设置总长度为len
sdssetalloc(s, len);
returns;
}

4.4 SDS 最长多少?

Redis 官方给出了最大的字符串容量为 512MB。这是为什么呢?

在 Reids3.x 版本中len是使用int修饰的,这就会导致 buf 最长就是2147483647,无形中限制了字符串的最大长度。

任何细节在源码中都能发现,在_sdsnewlen方法创建 SDS 中都会调用sdsTypeMaxSize方法获取每种类型所能创建的最大buf长度,不难发现此方法最大的返回值为2147483647,即512MB。

staticinlinesize_tsdsTypeMaxSize(chartype) {
if (type==SDS_TYPE_5)
return (1<<5) -1;
if (type==SDS_TYPE_8)
return (1<<8) -1;
if (type==SDS_TYPE_16)
return (1<<16) -1;
#if (LONG_MAX == LLONG_MAX)
if (type==SDS_TYPE_32)
return (1ll<<32) -1; //不管方法啥意思,最大返回2147483647。OVER~
#endif
return-1; /*thisisequivalenttothemaxSDS_TYPE_64orSDS_TYPE_32*/
}

此方法在 Redis3.0.0中是不存在的

4.5 部分 API 源码解读

创建SDS

Redis 通过sdsnewlen方法创建 SDS。在方法中会根据字符串初始化长度选择合适的类型。

sds_sdsnewlen(constvoid*init, size_tinitlen, inttrymalloc) {
void*sh;
sdss;
//根据初始化长度判断SDS的类型
chartype=sdsReqType(initlen);
//SDS_TYPE_5强制转换为SDS_TYPE_8
//这样侧面验证了sdshdr5从未被使用,创建这一步就GG了੯ੁૂ‧̀͡u\
if (type==SDS_TYPE_5&&initlen==0) type=SDS_TYPE_8;
//获取头部大学
inthdrlen=sdsHdrSize(type);
//指向flags的指针
unsignedchar*fp; /*flagspointer. */
//分配的空间
size_tusable;
//防止溢出
assert(initlen+hdrlen+1>initlen); /*Catchsize_toverflow*/
//分配空间
//s_trymalloc_usable: 尝试分配内存,失败则返回NULL
//s_malloc_usable: 分配内存或者抛异常[不友好]
sh=trymalloc?
s_trymalloc_usable(hdrlen+initlen+1, &usable) :
s_malloc_usable(hdrlen+initlen+1, &usable);
if (sh==NULL) returnNULL;
if (init==SDS_NOINIT)
init=NULL;
elseif (!init)
memset(sh, 0, hdrlen+initlen+1);
//s此时指向buf
s= (char*)sh+hdrlen;
//指向flags
fp= ((unsignedchar*)s)-1;
usable=usable-hdrlen-1;
//对不同类型的SDS可分配空间进行截断
if (usable>sdsTypeMaxSize(type))
usable=sdsTypeMaxSize(type);
switch(type) {
caseSDS_TYPE_5: {
*fp=type| (initlen<<SDS_TYPE_BITS);
break;
}
caseSDS_TYPE_8: {
SDS_HDR_VAR(8,s);
sh->len=initlen;
sh->alloc=usable;
*fp=type;
break;
}
//...省略
}
if (initlen&&init)
memcpy(s, init, initlen);
//末尾添加\0
s[initlen] ='\0';
returns;
}

通过sdsnewlen方法我们可以获得以下信息:

  • SDS_TYPE_5 会被强制转换为 SDS_TYPE_8 类型;
  • 创建时默认会在末尾加’\0′;
  • 返回值是指向 SDS 结构中 buf 的指针;
  • 返回值是char *sds类型,可以兼容部分C函数。

释放SDS

为了优化性能,SDS 提供了不直接释放内存,而是通过重置len达到清空 SDS 目的的方法——sdsclear。

改方法仅仅将 SDS 的len归零,而buf的空间并为真正被清空,新的数据可以复写,而不用重新申请内存。

voidsdsclear(sdss) {
sdssetlen(s, 0);//设置len为0
s[0] ='\0';//“清空”buf
}

若真正想清空 SDS 则可以调用sdsfree方法,底层通过调用s_free释放内存。

voidsdsfree(sdss) {
if (s==NULL) return;
s_free((char*)s-sdsHdrSize(s[-1]));
}

好了以上就是本期关于Redis sds源码解析的全部内容了,我是敖丙,我们下期见!

文章来源网络,作者:运维,如若转载,请注明出处:https://shuyeidc.com/wp/239003.html<

(0)
运维的头像运维
上一篇2025-04-23 05:10
下一篇 2025-04-23 05:11

相关推荐

  • 个人主题怎么制作?

    制作个人主题是一个将个人风格、兴趣或专业领域转化为视觉化或结构化内容的过程,无论是用于个人博客、作品集、社交媒体账号还是品牌形象,核心都是围绕“个人特色”展开,以下从定位、内容规划、视觉设计、技术实现四个维度,详细拆解制作个人主题的完整流程,明确主题定位:找到个人特色的核心主题定位是所有工作的起点,需要先回答……

    2025-11-20
    0
  • 社群营销管理关键是什么?

    社群营销的核心在于通过建立有温度、有价值、有归属感的社群,实现用户留存、转化和品牌传播,其管理需贯穿“目标定位-内容运营-用户互动-数据驱动-风险控制”全流程,以下从五个维度展开详细说明:明确社群定位与目标社群管理的首要任务是精准定位,需明确社群的核心价值(如行业交流、产品使用指导、兴趣分享等)、目标用户画像……

    2025-11-20
    0
  • 香港公司网站备案需要什么材料?

    香港公司进行网站备案是一个涉及多部门协调、流程相对严谨的过程,尤其需兼顾中国内地与香港两地的监管要求,由于香港公司注册地与中国内地不同,其网站若主要服务内地用户或使用内地服务器,需根据服务器位置、网站内容性质等,选择对应的备案路径(如工信部ICP备案或公安备案),以下从备案主体资格、流程步骤、材料准备、注意事项……

    2025-11-20
    0
  • 如何企业上云推广

    企业上云已成为数字化转型的核心战略,但推广过程中需结合行业特性、企业痛点与市场需求,构建系统性、多维度的推广体系,以下从市场定位、策略设计、执行落地及效果优化四个维度,详细拆解企业上云推广的实践路径,精准定位:明确目标企业与核心价值企业上云并非“一刀切”的方案,需先锁定目标客户群体,提炼差异化价值主张,客户分层……

    2025-11-20
    0
  • PS设计搜索框的实用技巧有哪些?

    在PS中设计一个美观且功能性的搜索框需要结合创意构思、视觉设计和用户体验考量,以下从设计思路、制作步骤、细节优化及交互预览等方面详细说明,帮助打造符合需求的搜索框,设计前的规划明确使用场景:根据网站或APP的整体风格确定搜索框的调性,例如极简风适合细线条和纯色,科技感适合渐变和发光效果,电商类则可能需要突出搜索……

    2025-11-20
    0

发表回复

您的邮箱地址不会被公开。必填项已用 * 标注