Redis学习之SDS源码分析
一.SDS的简单介绍
SDS:简单动态字符串(simple dynamic string)
2)SDS用来保存数据库中的字符串值
3)SDS被用作缓冲区:比如AOF模块的AOF缓冲区,以及客户端状态中的输入缓冲区
二.SDS的结构
struct sdshdr { // buf 中已占用空间的长度
int len; // buf 中剩余可用空间的长度
int free; // 字节数组
char buf[];
};
分析:
1)free=5:代表空闲空间长度为5
2)len=5:代表已经使用的空间长度为5
三.Redis使用SDS的原因
1)常数复杂度获取字符串长度:O(1)
C字符串获取字符串长度时间复杂度为O(N),使用SDS可以确保获取字符串长度的操作不会成为Redis的性能瓶颈
2)杜绝缓冲区溢出
C字符串不记录自身长度和空闲空间,容易造成缓冲区溢出,使用SDS则不会,SDS拼接字符串之前会先通过free字段检测剩余空间能否满足需求,不能满足需求的就会扩容
3)减少修改字符串时带来的内存重分配次数
使用C字符串的话:
每次对一个C字符串进行增长或缩短操作,长度都需要对这个C字符串数组进行一次内存重分配,比如C字符串的拼接,程序要先进行内存重分配来扩展字符串数组的大小,避免缓冲区溢出,又比如C字符串的缩短操作,程序需要通过内存重分配来释放不再使用的那部分空间,避免内存泄漏
使用SDS的话:
通过SDS的len属性和free属性可以实现两种内存分配的优化策略:空间预分配和惰性空间释放
1.针对内存分配的策略:空间预分配
在对SDS的空间进行扩展的时候,程序不仅会为SDS分配修改所必须的空间,还会为SDS分配额外的未使用的空间
这样可以减少连续执行字符串增长操作所需的内存重分配次数,通过这种预分配的策略,SDS将连续增长N次字符串所需的内存重分配次数从必定N次降低为最多N次,这是个很大的性能提升!
2.针对内存释放的策略:惰性空间释放
在对SDS的字符串进行缩短操作的时候,程序并不会立刻使用内存重分配来回收缩短之后多出来的字节,而是使用free属性将这些字节的数量记录下来等待将来使用,通过惰性空间释放策略,SDS避免了缩短字符串时所需的内存重分配次数,并且为将来可能有的增长操作提供了优化!
4)二进制安全
为了确保数据库可以二进制数据(图片,视频等),SDS的API都是二进制安全的,所有的API都会以处理二进制的方式来处理存放在SDS的buf数组里面的数据,程序不会对其中的数据做任何的限制,过滤,数据存进去是什么样子,读出来就是什么样子,这也是buf数组叫做字节数组而不是叫字符数组的原因,以为它是用来保存一系列二进制数据的
通过二进制安全的SDS,Redis不仅可以保存文本数据,还可以保存任意格式是二进制数
四.SDS的主要API及其源码解析
1)sdsnew函数:创建一个包含给定字符串的SDS
sds sdsnew(const char *init)
/*
* 根据给定字符串 init ,创建一个包含同样字符串的 sds
*
* 参数
* init :如果输入为 NULL ,那么创建一个空白 sds
* 否则,新创建的 sds 中包含和 init 内容相同字符串
*
* 返回值
* sds :创建成功返回 sdshdr 相对应的 sds
* 创建失败返回 NULL
*
* 复杂度
* T = O(N)
*/
sds sdsnew(const char *init) {
size_t initlen = (init == NULL) ? : strlen(init);
return sdsnewlen(init, initlen);
} /*
* 根据给定的初始化字符串 init 和字符串长度 initlen
* 创建一个新的 sds
*
* 参数
* init :初始化字符串指针
* initlen :初始化字符串的长度
*
* 返回值
* sds :创建成功返回 sdshdr 相对应的 sds
* 创建失败返回 NULL
*
* 复杂度
* T = O(N)
*/
sds sdsnewlen(const void *init, size_t initlen) { struct sdshdr *sh; // 根据是否有初始化内容,选择适当的内存分配方式
// T = O(N)
if (init) {
// zmalloc 不初始化所分配的内存
sh = zmalloc(sizeof(struct sdshdr) + initlen + );
}
else {
// zcalloc 将分配的内存全部初始化为 0
sh = zcalloc(sizeof(struct sdshdr) + initlen + );
} // 内存分配失败,返回
if (sh == NULL) return NULL; // 设置初始化长度
sh->len = initlen;
// 新 sds 不预留任何空间
sh->free = ;
// 如果有指定初始化内容,将它们复制到 sdshdr 的 buf 中
// T = O(N)
if (initlen && init)
memcpy(sh->buf, init, initlen);
// 以 \0 结尾
sh->buf[initlen] = '\0'; // 返回 buf 部分,而不是整个 sdshdr,因为sds是char指针类型的别名
return (char*)sh->buf;
}
2)sdsempty函数:创建一个不包含任何内容的SDS
sds sdsempty(void)
/*
* 创建并返回一个只保存了空字符串 "" 的 sds
*
* 返回值
* sds :创建成功返回 sdshdr 相对应的 sds
* 创建失败返回 NULL
*
* 复杂度
* T = O(1)
*/
sds sdsempty(void) {
return sdsnewlen("", );
}
3)sdsfree函数:释放给定的SDS
/*
* 释放给定的 sds
*
* 复杂度
* T = O(N)
*/
void sdsfree(sds s) {
if (s == NULL) return;
zfree(s - sizeof(struct sdshdr));
}
ps:zfree函数为内存管理模块中的函数,我们在这里先不探究,只需要知道它可以释放指定的空间就可以了
分析:s - sizeof(struct sdshdr)到底返回的是什么呢?
首先我们知道SDS的buf数组是柔性数组,也就是这个数组是不占据内存大小的,所以sizeof(struct sdshdr)
为8
还有SDS数据类型为char *类型,所以假如存在这样一条语句:
struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)))
其结构的内存结构图为:
所以s-(sizeof(struct sdshdr))就是指向sds的头部的!!
4)sdslen函数:返回SDS的已使用的空间字节数
/*
* 返回 sds 已经使用的空间字节数
*
* T = O(1)
*/
static inline size_t sdslen(const sds s)
{
struct sdshdr *sh = (void*)(s - (sizeof(struct sdshdr)));
return sh->len;
}
static inline修饰的函数是内联函数,目的是解决函数在多次调用时候的效率问题!
size_t是无符号整数,是sizeof操作符返回的结构类型
const代表变量只能读,不能被修改
5)sdsavail函数:返回SDS的未使用的空间字节数
/*
* 返回 sds 为使用的空间字节数
*
* T = O(1)
*/
static inline size_t sdsavail(const sds s)
{
struct sdshdr *sh = (void*)(s - (sizeof(struct sdshdr)));
return sh->free;
}
跟sdslen函数的区别就在于返回的属性不同
6)sdsup函数:创建一个给定SDS的副本(copy)
/*
* 复制给定 sds 的副本
*
* 返回值
* sds :创建成功返回输入 sds 的副本
* 创建失败返回 NULL
*
* 复杂度
* T = O(N)
*/
sds sdsdup(const sds s) {
return sdsnewlen(s, sdslen(s));
}
调用了sdsnewlen函数和sdslen函数,这两个函数在上面都有介绍
7)sdsclear函数:清空SDS保存的字符串内容
/*
* 在不释放 SDS 的字符串空间的情况下,
* 重置 SDS 所保存的字符串为空字符串。
*
* 复杂度
* T = O(1)
*/
void sdsclear(sds s) { // 取出 sdshdr
struct sdshdr *sh = (void*)(s - (sizeof(struct sdshdr))); // 重新计算属性
sh->free += sh->len;
sh->len = ; // 将结束符放到最前面(相当于惰性地删除 buf 中的内容)
sh->buf[] = '\0';
}
以上采用了惰性空间释放的策略,其实buf中的内容并没有被“真正的删除”,只是len属性和free属性变了,结束符移动到buf数组最前面了而已
8)sdscat函数:将给定的C字符串拼接到SDS字符串的末尾
/*
* 将给定字符串 t 追加到 sds 的末尾
*
* 返回值
* sds :追加成功返回新 sds ,失败返回 NULL
*
* 复杂度
* T = O(N)
*/
sds sdscat(sds s, const char *t) {
return sdscatlen(s, t, strlen(t));
}
/*
* 将长度为 len 的字符串 t 追加到 sds 的字符串末尾
*
* 返回值
* sds :追加成功返回新 sds ,失败返回 NULL
*
* 复杂度
* T = O(N)
*/
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'; // 返回新 sds
return s;
}
/*
* 对 sds 中 buf 的长度进行扩展,确保在函数执行之后,
* buf 至少会有 addlen + 1 长度的空余空间
* (额外的 1 字节是为 \0 准备的)
*
* 返回值
* sds :扩展成功返回扩展后的 sds
* 扩展失败返回 NULL
*
* 复杂度
* T = O(N)
*/
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 *= ;
else
// 否则,分配长度为目前长度加上 SDS_MAX_PREALLOC
newlen += SDS_MAX_PREALLOC;
// T = O(N)
newsh = zrealloc(sh, sizeof(struct sdshdr) + newlen + ); // 内存不足,分配失败,返回
if (newsh == NULL) return NULL; // 更新 sds 的空余长度
newsh->free = newlen - len; // 返回 sds
return newsh->buf;
}
sdscat函数调用了sdscatlen函数,sdscatlen函数调用sdsMakeRoomFor函数
我们可以发现sdsMakeRoomFor函数采用了空间预分配的策略,确保在对buf进行长度扩展的时候至少为有addlen+1长度的空余空间,addlen为buf后面拼接的C字符串长度
sdsMakeRoomFor函数中有个宏定义的常量SDS_MAX_PREALLOC ,为最大预先分配长度,是为空间预分配策略服务的
sdsMakeRoomFor函数中调用了zrealloc函数,zrealloc函数的作用是分配指定内存大小空间给sds,此函数属于内存分配模块,我们先不探究
9)sdscatsds函数:将给定的SDS字符串拼接到另一个SDS字符串的末尾
/*
* 将另一个 sds 追加到一个 sds 的末尾
*
* 返回值
* sds :追加成功返回新 sds ,失败返回 NULL
*
* 复杂度
* T = O(N)
*/ sds sdscatsds(sds s, const sds t) {
return sdscatlen(s, t, sdslen(t));
}
sdscatsds函数调用的两个函数:sdscatlen和sdslen
已经在上面解析过了,这里不再解析
10)sdscpy函数:将给定的C字符串复制到SDS里面,覆盖SDS原有的字符串
/*
* 将字符串复制到 sds 当中,
* 覆盖原有的字符。
*
* 如果 sds 的长度少于字符串的长度,那么扩展 sds 。
*
* 复杂度
* T = O(N)
*
* 返回值
* sds :复制成功返回新的 sds ,否则返回 NULL
*/ sds sdscpy(sds s, const char *t) {
return sdscpylen(s, t, strlen(t));
}
/*
* 将字符串 t 的前 len 个字符复制到 sds s 当中,
* 并在字符串的最后添加终结符。
*
* 如果 sds 的长度少于 len 个字符,那么扩展 sds
*
* 复杂度
* T = O(N)
*
* 返回值
* sds :复制成功返回新的 sds ,否则返回 NULL
*/ sds sdscpylen(sds s, const char *t, size_t len) { struct sdshdr *sh = (void*)(s - (sizeof(struct sdshdr))); // sds 现有 buf 的长度
size_t totlen = sh->free + sh->len; // 如果 s 的 buf 长度不满足 len ,那么扩展它
if (totlen < len) {
// T = O(N)
s = sdsMakeRoomFor(s, len - sh->len); //扩展失败,返回NULL
if (s == NULL) return NULL; //扩展成功
sh = (void*)(s - (sizeof(struct sdshdr)));
totlen = sh->free + sh->len;
} // 复制内容
// T = O(N)
memcpy(s, t, len); // 添加终结符号
s[len] = '\0'; // 更新属性
sh->len = len;
sh->free = totlen - len; // 返回新的 sds
return s;
}
sdscpy函数调用了sdscpylen函数,sdscpylen函数又调用了sdsMakeRoomFor函数
代码分析看注释就好
11)sdsgrowzero函数:用空字符将SDS扩展至给定长度
/*
* 将 sds 扩充至指定长度,未使用的空间以 0 字节填充。
*
* 返回值
* sds :扩充成功返回新 sds ,失败返回 NULL
*
* 复杂度:
* T = O(N)
*/
sds sdsgrowzero(sds s, size_t len) { struct sdshdr *sh = (void*)(s - (sizeof(struct sdshdr)));//*sh指向s的头部 size_t totlen, curlen = sh->len;//总长度 已经使用的长度 // 如果 len 比字符串的现有长度小,
// 那么直接返回,不做动作
if (len <= curlen) return s; // 扩展 sds
// T = O(N)
s = sdsMakeRoomFor(s, len - curlen); // 如果内存不足,直接返回
if (s == NULL) return NULL; // 将新分配的空间用 0 填充,防止出现垃圾内容
// T = O(N)
sh = (void*)(s - (sizeof(struct sdshdr)));
memset(s + curlen, , (len - curlen + )); // also set trailing \0 byte // 更新属性
totlen = sh->len + sh->free;
sh->len = len;
sh->free = totlen - sh->len; // 返回新的 sds
return s;
}
没有判断len-curlen和free的关系,就算free>=len-curlen也会进行一次扩容操作
12.sdsrange函数:保留SDS给定区间内的数据,不在区间内的数据会被覆盖或者清除
/*
* 按索引对截取 sds 字符串的其中一段
* start 和 end 都是闭区间(包含在内)
*
* 索引从 0 开始,最大为 sdslen(s) - 1
* 索引可以是负数, sdslen(s) - 1 == -1
*
* 复杂度
* T = O(N)
*/
/*
* s = sdsnew("Hello World");
* sdsrange(s,1,-1); => "ello World"
*/
void sdsrange(sds s, int start, int end) { //sh指向s头部
struct sdshdr *sh = (void*)(s - (sizeof(struct sdshdr))); size_t newlen, len = sdslen(s); //没有可以截取的字符串,直接返回
if (len == ) return; //start参数规则
if (start < ) {
start = len + start;
if (start < ) start = ;
} //end参数规则
if (end < ) {
end = len + end;
if (end < ) end = ;
} //len取决于start和end的关系
newlen = (start > end) ? : (end - start) + ; //新的sds的len!=0
if (newlen != ) { //需要截取的起点大于等于有符号的len 那么新的sds的len=0
if (start >= (signed)len) {
newlen = ;
} //终点超出了有符号的len 终点就是len-1
else if (end >= (signed)len) {
end = len - ; //重新计算len
newlen = (start > end) ? : (end - start) + ;
}
}
else {
start = ;
} // 如果有需要,对字符串进行移动
// T = O(N)
if (start && newlen) memmove(sh->buf, sh->buf + start, newlen); // 添加终结符
sh->buf[newlen] = ; // 更新属性
sh->free = sh->free + (sh->len - newlen);
sh->len = newlen;
}
注意start和end参数的设置,以及有时候需要对字符串进行移动操作
13.sdstrim函数:接受一个SDS和一个C字符串作为参数,从SDS左右两端分别移除所有在C字符串中出现过的字符
/*
* 对 sds 左右两端进行修剪,清除其中 cset 指定的所有字符
*
* 在头部遇到某个字符不属于cset,则头部清除停下
* 在尾部遇到某个字符不属于cset,则尾部清除停下
*
* 比如 sdsstrim(xxyayabcycyxy, "xy") 将返回 "ayabcyc"
*
* 复杂性:
* T = O(M*N),M 为 SDS 长度, N 为 cset 长度。
* Example:
*
* s = sdsnew("AA...AA.a.aa.aHelloWorld :::");
* s = sdstrim(s,"A. :");
* printf("%s\n", s);
*
* Output will be just "a.aa.aHello World".
*/
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) - ; // 修剪, T = O(N^2)
while (sp <= end && strchr(cset, *sp)) sp++; //从头部开始清除,遇到第一个不属于cset里面的字符则停止头部清除工作
while (ep > start && strchr(cset, *ep)) ep--;//从尾部开始清除,遇到第一个不属于cset里面的字符则停止尾部清除工作 // 计算 trim 完毕之后剩余的字符串长度
len = (sp > ep) ? : ((ep - sp) + ); // 如果有需要,前移字符串内容
// 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; // 返回修剪后的 sds
return s;
}
注意sdstrim函数不是移除所有在子串中出现过的字符,而是移除头部和尾部在子串中出现的字符
举个例子:比如 sdsstrim(xxyayabcycyxy, "xy") 将返回 "ayabcyc"
14)sdscmp函数:比较两个SDS字符串是否相等
/*
* 对比两个 sds , strcmp 的 sds 版本
*
* 返回值
* int :相等返回 0 ,s1 较大返回正数, s2 较大返回负数
*
* T = O(N)
*/
int sdscmp(const sds s1, const sds s2) {
size_t l1, l2, minlen;
int cmp; //s1长度
l1 = sdslen(s1); //s2长度
l2 = sdslen(s2); //s1和s2最小长度
minlen = (l1 < l2) ? l1 : l2; //比较
cmp = memcmp(s1, s2, minlen); //s1较大返回正数,s2较大返回负数,相等返回0
if (cmp == ) return l1 - l2; return cmp;
}
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
参考:《Redis设计与实现》
Redis学习之SDS源码分析的更多相关文章
- Redis学习——ae事件处理源码分析
0. 前言 Redis在封装事件的处理采用了Reactor模式,添加了定时事件的处理.Redis处理事件是单进程单线程的,而经典Reator模式对事件是串行处理的.即如果有一个事件阻塞过久的话会导致整 ...
- Redis学习之字典源码分析
字典,又叫映射,是一种用于保存键值对的抽象数据结构 划重点:抽象数据结构 Redisd字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表结点,而每个哈希表结点就保存了字典中的一个键值对 一.哈 ...
- Nginx学习笔记4 源码分析
Nginx学习笔记(四) 源码分析 源码分析 在茫茫的源码中,看到了几个好像挺熟悉的名字(socket/UDP/shmem).那就来看看这个文件吧!从简单的开始~~~ src/os/unix/Ngx_ ...
- MQTT再学习 -- MQTT 客户端源码分析
MQTT 源码分析,搜索了一下发现网络上讲的很少,多是逍遥子的那几篇. 参看:逍遥子_mosquitto源码分析系列 参看:MQTT libmosquitto源码分析 参看:Mosquitto学习笔记 ...
- 【Redis】事件驱动框架源码分析
aeEventLoop初始化 在server.c文件的initServer函数中,对aeEventLoop进行了初始化: 调用aeCreateEventLoop函数创建aeEventLoop结构体,对 ...
- 【Redis】事件驱动框架源码分析(单线程)
aeEventLoop初始化 在server.c文件的initServer函数中,对aeEventLoop进行了初始化: 调用aeCreateEventLoop函数创建aeEventLoop结构体,对 ...
- Java多线程学习之ThreadLocal源码分析
0.概述 ThreadLocal,即线程本地变量,是一个以ThreadLocal对象为键.任意对象为值的存储结构.它可以将变量绑定到特定的线程上,使每个线程都拥有改变量的一个拷贝,各线程相同变量间互不 ...
- springMVC源码学习之addFlashAttribute源码分析
本文主要从falshMap初始化,存,取,消毁来进行源码分析,springmvc版本4.3.18.关于使用及验证请参考另一篇jsp取addFlashAttribute值深入理解即springMVC发r ...
- 大数据学习--day14(String--StringBuffer--StringBuilder 源码分析、性能比较)
String--StringBuffer--StringBuilder 源码分析.性能比较 站在优秀博客的肩上看问题:https://www.cnblogs.com/dolphin0520/p/377 ...
随机推荐
- 【代码笔记】Web-Javascript-Javascript typeof
一,效果图. 二,代码. <!DOCTYPE html> <html> <head> <meta charset="utf-8"> ...
- iphone屏幕镜像 屏幕镜像怎么用
iPhone手机投屏到电脑设备上,需要使用到AirPlay镜像功能,但是有些苹果新用户朋友一定还不知道iphone屏幕镜像怎么用吧?你要着急下面为你提供解决方法. 使用工具:iphone.电脑 操作方 ...
- android开发——Android开发中的47个小知识
1.判断sd卡是否存在 boolean sdCardExist = Environment.getExternalStorageState().equals(android.os.Environme ...
- 取消IE、Office、Wmp首次开启提示
一.取消IE首次开启提示 1.运行框输入gpedit.msc.打开组策略配置 2.本地计算机策略-计算机配置-管理模板-windows组件-Internet Explorer,查找右边“阻止执行首次运 ...
- Wu反走样算法绘制直线段
Wu反走样算法 原理:在我看来,Wu反走样算法是在Bresenham算法基础上改进了一番,它给最靠近理想直线/曲线的两个点以不同的亮度值,以达到模糊锯齿的效果.因为人眼看到的是线附近亮度的平均值. M ...
- git 入门教程之版本控制
版本控制 我们知道 git 是分布式版本控制系统,所以称被控制对象是版本本身没错,但是从git 命令中发现,并没有版本这个名词,有的只是commit,所以前几节我一直称其为提交. 为了避免后续教程引发 ...
- (一)Maven简介
Maven这个词可以翻译为“知识的积累”,也可以翻译为“专家”或“内行”,是一个跨平台的项目管理工具.Maven主要服务于基于Java平台的项目构建.依赖管理和项目信息管理. 构建(build)是每一 ...
- 下载安装Emacs和基本配置--待更新中
Emacs下载地址 下载好后,解压到D:\Emacs,并设置环境变量D:\Emacs\bin 熟悉基本操作 光标的移动 文件的创建与打开
- Sql Server 按格式输出日期
SELECT dbo.fn_Data(getdate(),'yyyymmdd') CREATE FUNCTION [dbo].[fn_Data] (@date as datetime, @format ...
- Android 闪烁动画
import android.view.View; import android.view.animation.AlphaAnimation; import android.view.animatio ...