redis 版本:5.0

本文代码在Redis源码中的位置:redis/src/sds.c、redis/src/sds.h

源码整体结构

src:核心实现代码,用 C 语言编写

tests:单元测试代码,用 Tcl 实现

deps:所有依赖库

字符串存储结构

Redis 将字符串的实现称为 sds(simple dynamic string)。为了提高存储空间的利用率,Redis 对不同长度的字符串,采用不同的数据结构。

以下是长度小于32的字符串的存储结构:

struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags;
char buf[];
};

其中,flags:高5位指示字符串真实长度,低3位指示数据类型;buf:柔性数组,指向字符串存储地址。

:柔性数组是在 C99 及以上标准才支持,其目的是为了在结构体中能够“动态”地设定数组的长度。但需要注意的是,对结构体进行 sizeof 时,柔性数组的大小不被计算在内。因此在对结构体分配大小时,需要注意加上柔性数组的大小。柔性数组必须被声明在结构体的最后,其起始地址与上一字段的末尾地址相连。

参考:https://en.wikipedia.org/wiki/Flexible_array_member

其他长度的字符串的存储结构与其类似,结构分别如下:

struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* used */
uint8_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
}; struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len; /* used */
uint16_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
}; struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len; /* used */
uint32_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
}; struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len; /* used */
uint64_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};

其中,len 表示字符串实际长度;alloc 表示申请的字符数组的长度。

可以看出,Redis 最长能存储长度大约为 \(1.84* 10^{19}\) (\(2^{64}-1\)) 的字符串。

:在声明结构体时,struct __attribute__ ((__packed__)) 语法用来告诉编译器采用紧凑的方式存储数据,即1字节对齐,而不是默认的所有变量大小的最小公倍数做字节对齐。

sdshdr32 为例,若采用1字节对齐,lenallocflags 一共占9个字节(4+4+1),而默认会使用4字节对齐,这样 flags 也会占用4个字节,一共占用12个字节。

采用 packed 的好处是明显的,一来可以减少数据的大小,提高空间利用率;二来这为地址计算带来了方便:无论哪种类型(除 sdshdr5 外),使用 buff[-1] 即能获取到 flag

字符串基本操作

创建

sdsnewlen 方法负责创建字符串,代码如下:

sds sdsnewlen(const void *init, size_t initlen) {
void *sh;
sds s;
char type = sdsReqType(initlen);
if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
int hdrlen = sdsHdrSize(type);
unsigned char *fp; sh = s_malloc(hdrlen+initlen+1);
if (init==SDS_NOINIT)
init = NULL;
else if (!init)
memset(sh, 0, hdrlen+initlen+1);
if (sh == NULL) return NULL;
s = (char*)sh+hdrlen;
fp = ((unsigned char*)s)-1;
switch(type) {
case SDS_TYPE_5: {
*fp = type | (initlen << SDS_TYPE_BITS);
break;
}
case SDS_TYPE_8: {
SDS_HDR_VAR(8,s);
sh->len = initlen;
sh->alloc = initlen;
*fp = type;
break;
}
case SDS_TYPE_16: {
SDS_HDR_VAR(16,s);
sh->len = initlen;
sh->alloc = initlen;
*fp = type;
break;
}
case SDS_TYPE_32: {
SDS_HDR_VAR(32,s);
sh->len = initlen;
sh->alloc = initlen;
*fp = type;
break;
}
case SDS_TYPE_64: {
SDS_HDR_VAR(64,s);
sh->len = initlen;
sh->alloc = initlen;
*fp = type;
break;
}
}
if (initlen && init)
memcpy(s, init, initlen);
s[initlen] = '\0';
return s;
}

以上代码比较简单,但有几点需要说明:

  • 从第5行代码可以看出,对于空字符串,Redis 将其做为 sdshdr8 类型而不是 sdshdr5 类型。其给出的原因是:用户很有可能会在空字符串后追加新的字符串,因此用一个存储长度适中的结构。
  • sh 指针指向对应结构体的起始地址,其长度为结构体大小 + 待存储字符串的大小 + 1。最后加1是为了在末尾追加一个 \0 符。
  • s 指针即为以上提到的柔性数组起始地址。fp 指针地址正好在 s 指针指向地址的前一字节,指向 flags
  • 第19行代码中,initlen << SDS_TYPE_BITSinitlen 左移3位,正好印证之前提到的 flags 使用高5位存储字符串长度。之后或上 type ,即把低3位置为 type 的值(因为之前左移3位后,低3位值均为0,这样或运算的值就是 type)。
  • SDS_HDR_VAR 宏定义为 #define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T))); 。其中的 ## 表示字符连接的意思。以23行代码为例,该行代码在经编译器预处理后,变为 struct sdshdr8 *sh = (void*)((s)-(sizeof(struct sdshdr8))),语义为sh = s - sizeof(sdshdr8)暂时还没弄明白这里为啥要重算一下 sh。按理说第15行代码 s = (char*)sh+hdrlen; 中,s 正是由 sh +sizeof(sdshdr8)得到。
  • 注意,最终返回给外部的是 s 指针而不是结构体指针。

其中的sdsReqType 方法用于根据待存储字符串长度选择合适的存储类型,代码如下:

static inline char sdsReqType(size_t string_size) {
if (string_size < 1<<5)
return SDS_TYPE_5;
if (string_size < 1<<8)
return SDS_TYPE_8;
if (string_size < 1<<16)
return SDS_TYPE_16;
#if (LONG_MAX == LLONG_MAX)
if (string_size < 1ll<<32)
return SDS_TYPE_32;
return SDS_TYPE_64;
#else
return SDS_TYPE_32;
#endif
}

LONG_MAX 表示 long 类型整数最大值,与编译器有关。通常64位编译器值为 \(2^{63} - 1\),32位编译器为 \(2^{32}-1\)。LLONG_MAX 表示 long long 类型整数最大值,通常32位和64位编译器值均为 \(2^{63} - 1\)。

参考:

:static inline 关键字是在建议编译器,以类似于宏定义的方式对待该函数,即在编译阶段,直接将该函数的相关指令插入到调用该函数的地方。这样做可减少函数调用的开销。

参考:https://zhuanlan.zhihu.com/p/132726037

sdsHdrSize 函数用于计算对应存储类型的大小,代码如下:

static inline int sdsHdrSize(char type) {
switch(type&SDS_TYPE_MASK) {
case SDS_TYPE_5:
return sizeof(struct sdshdr5);
case SDS_TYPE_8:
return sizeof(struct sdshdr8);
case SDS_TYPE_16:
return sizeof(struct sdshdr16);
case SDS_TYPE_32:
return sizeof(struct sdshdr32);
case SDS_TYPE_64:
return sizeof(struct sdshdr64);
}
return 0;
}

其中,SDS_TYPE_MASK 的值为7,二进制即为:0000 0111type 和它做与运算,正好得出自己的后3位的值,即类型值。

删除

删除字符串的方法有两种,一种是真正释放了内存,代码如下:

void sdsfree(sds s) {
if (s == NULL) return;
s_free((char*)s-sdsHdrSize(s[-1]));
}

其中 s[-1] 之前也提到过,就是 flags 的值。而 s-sdsHdrSize(s[-1]) 则得到了 sdshdr 结构体的起始地址。

另一种则只是将长度置为0,并没有真正释放内存,这么做的目的当然是为了下次存储字符串时,无需重新申请内存,直接再用即可。

void sdsclear(sds s) {
sdssetlen(s, 0);
s[0] = '\0';
}

其中,sdssetlen 函数用来设置字符串长度,代码如下:

static inline void sdssetlen(sds s, size_t newlen) {
unsigned char flags = s[-1];
switch(flags&SDS_TYPE_MASK) {
case SDS_TYPE_5:
{
unsigned char *fp = ((unsigned char*)s)-1;
*fp = SDS_TYPE_5 | (newlen << SDS_TYPE_BITS);
}
break;
case SDS_TYPE_8:
SDS_HDR(8,s)->len = newlen;
break;
case SDS_TYPE_16:
SDS_HDR(16,s)->len = newlen;
break;
case SDS_TYPE_32:
SDS_HDR(32,s)->len = newlen;
break;
case SDS_TYPE_64:
SDS_HDR(64,s)->len = newlen;
break;
}
}

追加(cat)

追加操作用于在一个字符串后面添加另一字符串,其代码如下:

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

连接后,s 将指向连接后的字符串。

sdscatlen 函数逻辑也很简单:扩容(若需要) -> 复制追加内容 -> 修改长度。代码如下:

sds sdscatlen(sds s, const void *t, size_t len) {
size_t curlen = sdslen(s);
s = sdsMakeRoomFor(s,len);
if (s == NULL) return NULL;
memcpy(s+curlen, t, len);
sdssetlen(s, curlen+len);
s[curlen+len] = '\0';
return s;
}

其中 sdsMakeRoomFor 为扩容函数,根据剩余可用空间大小和待追加的字符串长度决定是否扩容。它保证了其返回的指针指向的内存区域,一定能容纳追加的字符串(若内部的内存申请成功的话)。代码如下:

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; /* Return ASAP if there is enough space left. */
if (avail >= addlen) return s; len = sdslen(s);
sh = (char*)s-sdsHdrSize(oldtype);
newlen = (len+addlen);
if (newlen < SDS_MAX_PREALLOC)
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC; type = sdsReqType(newlen); /* Don't use type 5: the user is appending to the string and type 5 is
* not able to remember empty space, so sdsMakeRoomFor() must be called
* at every appending operation. */
if (type == SDS_TYPE_5) type = SDS_TYPE_8; hdrlen = sdsHdrSize(type);
if (oldtype==type) {
newsh = s_realloc(sh, hdrlen+newlen+1);
if (newsh == NULL) return NULL;
s = (char*)newsh+hdrlen;
} else {
/* Since the header size changes, need to move the string forward,
* and can't use 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;
s[-1] = type;
sdssetlen(s, len);
}
sdssetalloc(s, newlen);
return s;
}

其基本逻辑为:

  • 若剩余容量足够容纳待追加的字符串,则无需扩容(第9行)。
  • 否则,算出追加后的字符串的长度 newlen(第13行)。第13行~第17行代码还对 newlen 做了调整。暂时不知道为啥要这么做
  • 之后根据 newlen 决定使用什么类型进行存储。若类型还和之前一样,则继续沿用之前的内存结构,但需要使用 s_realloc 扩大之前的内存空间(第28行);否则则需要重新申请一块新的内存(第34行),将之前的数据复制到新内存中(第36行)。

连接(join)

连接操作是将一个字符串数组的所有元素串联在一起,若给定了分隔符,各元素之间还需插入分隔符。例如,若字符串数组 s = ["a", "b", "c"] ,分隔符 sep = ",",则 join(s, sep) 的结果为:a,b,c。其代码如下:

sds sdsjoin(char **argv, int argc, char *sep) {
sds join = sdsempty();
int j; for (j = 0; j < argc; j++) {
join = sdscat(join, argv[j]);
if (j != argc-1) join = sdscat(join,sep);
}
return join;
}

代码很简单,就不多做说明了。其中,sdsempty() 是构造一个空串,即:sdsnewlen("",0)

分割参数

sdssplitargs 函数用于将一条命令按参数分割成一个字符串数组,代码如下:

sds *sdssplitargs(const char *line, int *argc) {
const char *p = line;
char *current = NULL;
char **vector = NULL; *argc = 0;
while(1) {
/* skip blanks */
while(*p && isspace(*p)) p++;
if (*p) {
/* get a token */
int inq=0; /* set to 1 if we are in "quotes" */
int insq=0; /* set to 1 if we are in 'single quotes' */
int done=0; if (current == NULL) current = sdsempty();
while(!done) {
if (inq) {
if (*p == '\\' && *(p+1) == 'x' &&
is_hex_digit(*(p+2)) &&
is_hex_digit(*(p+3)))
{
unsigned char byte; byte = (hex_digit_to_int(*(p+2))*16)+
hex_digit_to_int(*(p+3));
current = sdscatlen(current,(char*)&byte,1);
p += 3;
} else if (*p == '\\' && *(p+1)) {
char c; p++;
switch(*p) {
case 'n': c = '\n'; break;
case 'r': c = '\r'; break;
case 't': c = '\t'; break;
case 'b': c = '\b'; break;
case 'a': c = '\a'; break;
default: c = *p; break;
}
current = sdscatlen(current,&c,1);
} else if (*p == '"') {
/* closing quote must be followed by a space or
* nothing at all. */
if (*(p+1) && !isspace(*(p+1))) goto err;
done=1;
} else if (!*p) {
/* unterminated quotes */
goto err;
} else {
current = sdscatlen(current,p,1);
}
} else if (insq) {
if (*p == '\\' && *(p+1) == '\'') {
p++;
current = sdscatlen(current,"'",1);
} else if (*p == '\'') {
/* closing quote must be followed by a space or
* nothing at all. */
if (*(p+1) && !isspace(*(p+1))) goto err;
done=1;
} else if (!*p) {
/* unterminated quotes */
goto err;
} else {
current = sdscatlen(current,p,1);
}
} else {
switch(*p) {
case ' ':
case '\n':
case '\r':
case '\t':
case '\0':
done=1;
break;
case '"':
inq=1;
break;
case '\'':
insq=1;
break;
default:
current = sdscatlen(current,p,1);
break;
}
}
if (*p) p++;
}
/* add the token to the vector */
vector = s_realloc(vector,((*argc)+1)*sizeof(char*));
vector[*argc] = current;
(*argc)++;
current = NULL;
} else {
/* Even on empty input string return something not NULL. */
if (vector == NULL) vector = s_malloc(sizeof(void*));
return vector;
}
} err:
while((*argc)--)
sdsfree(vector[*argc]);
s_free(vector);
if (current) sdsfree(current);
*argc = 0;
return NULL;
}

以上代码总体上是在对字符串 line 逐个字符进行遍历,指针 p 做为游标指向当前访问的字符。

主体上,代码由两层 while 循环构成。外层 while 循环(第7行)条件总是成立,因此该函数要么出错结束(第102行~末尾),返回 NULL,要么从第98行成功结束,此时的条件是 p 指针正好指向字符串末尾的 \0,返回分割出的参数数组 vector。而内层 while 循环执行结束后(第91行),会识别出一个参数,并会将该参数添加到 vector 数组中(第92行)。

再来看内层 while 循环是如何识别一个参数的。这段代码(第17行到89行)总体结构是 if...else if ... else ...。进入前两分支的条件是处于双引号模式或单引号模式下,否则进入 else 分支。每轮循环结束,游标 p 向后挪动一位(第88行)。

双、单引号模式由 inqinsq 变量指示,前者表示当前访问的字符在单引号内(即之前已访问了起始单引号,但还未访问到闭合单引号),后者则表示是在双引号内。进入这两个模式的时机当然是遇到了双引号(第77行)或单引号(第80行)。

在非双、单引号模式下,若遇到一般字符,都会将该字符追加到 current 字符串中(current 在遍历开始是空串);若遇到 空格'\n''\r''\t''\0',内层循环都将结束,并将 current 做为识别到的一个参数。

在双、单引号模式下,若是遇到 "\n""\r""\t""\b""\a"(注意,这些都是两个字符而不是一个),会将其做为单字符 '\n''\r''\t''\b''\a' 加入 current 中(第41行);若字符串遍历完成都没遇到闭合的双、单引号,则会报错(第49行和第64行);闭合的双、单引号后面不是空格(若字符串后面还有其他字符),也会报错(第44行和第60行)。

另外,在双引号模式下,会将格式类似于 "\x41"的子串,做为16进制数,转成字符类型后追加到 current 中(第19行~第28行)。该例中,"\x41" 将做为字母 A

以上便是 Redis 字符串操作最主要的函数,还有一些比如字符串大小写转换,比较等函数实现均非常简单,这里不再赘述,有兴趣可以去看看源码。

参考

Redis源码学习(1)──字符串的更多相关文章

  1. Redis源码学习:字符串

    Redis源码学习:字符串 1.初识SDS 1.1 SDS定义 Redis定义了一个叫做sdshdr(SDS or simple dynamic string)的数据结构.SDS不仅用于 保存字符串, ...

  2. Redis源码学习:Lua脚本

    Redis源码学习:Lua脚本 1.Sublime Text配置 我是在Win7下,用Sublime Text + Cygwin开发的,配置方法请参考<Sublime Text 3下C/C++开 ...

  3. 柔性数组(Redis源码学习)

    柔性数组(Redis源码学习) 1. 问题背景 在阅读Redis源码中的字符串有如下结构,在sizeof(struct sdshdr)得到结果为8,在后续内存申请和计算中也用到.其实在工作中有遇到过这 ...

  4. redis源码学习之slowlog

    目录 背景 环境说明 redis执行命令流程 记录slowlog源码分析 制造一条slowlog slowlog分析 1.slowlog如何开启 2.slowlog数量限制 3.slowlog中的耗时 ...

  5. __sync_fetch_and_add函数(Redis源码学习)

    __sync_fetch_and_add函数(Redis源码学习) 在学习redis-3.0源码中的sds文件时,看到里面有如下的C代码,之前从未接触过,所以为了全面学习redis源码,追根溯源,学习 ...

  6. redis源码学习之lua执行原理

    聊聊redis执行lua原理 从一次面试场景说起   "看你简历上写的精通redis" "额,还可以啦" "那你说说redis执行lua脚本的原理&q ...

  7. redis源码学习之工作流程初探

    目录 背景 环境准备 下载redis源码 下载Visual Studio Visual Studio打开redis源码 启动过程分析 调用关系图 事件循环分析 工作模型 代码分析 动画演示 网络模块 ...

  8. Redis源码阅读-sds字符串源码阅读

    redis使用sds代替char *字符串, 其定义如下: typedef char *sds; struct sdshdr { unsigned int len; unsigned int free ...

  9. redis源码学习_简单动态字符串

    SDS相比传统C语言的字符串有以下好处: (1)空间预分配和惰性释放,这就可以减少内存重新分配的次数 (2)O(1)的时间复杂度获取字符串的长度 (3)二进制安全 主要总结一下sds.c和sds.h中 ...

  10. Redis源码学习-Master&Slave的命令交互

    0. 写在前面 Version Redis2.2.2 Redis中可以支持主从结构,本文主要从master和slave的心跳机制出发(PING),分析redis的命令行交互. 在Redis中,serv ...

随机推荐

  1. NC16671 [NOIP2006]金明的预算方案

    题目链接 题目 题目描述 金明今天很开心,家里购置的新房就要领钥匙了,新房里有一间金明自己专用的很宽敞的房间.更让他高兴的是,妈妈昨天对他说:"你的房间需要购买哪些物品,怎么布置,你说了算, ...

  2. Idea 本人开发常用几款插件

    先说 idea装插件 首先,进入插件安装界面: 标注 1:显示 IntelliJ IDEA 的插件分类, All plugins:显示 IntelliJ IDEA 支持的所有插件: Enabled:显 ...

  3. Spring boot项目实战之记录应用访问日志

    1.说明 系统上线后往往我们需要知道都有哪些用户访问了应用的那些功能,以便更好的了解用户需求.防止恶意访问等.为此我们需要给应用添加记录访问日志的功能.下面就开始吧: 2.建表 CREATE TABL ...

  4. Java I/O 教程(七) DataOutputStream和DataInputStream

    Java DataOutputStream Class Java DataOutputStream class 可以以机器无关方式往指定输出流写入Java原始数据类型,例如int, double, l ...

  5. 图片Base64编码解码的优缺点及应用场景分析

    随着互联网的迅猛发展,图片在网页和移动应用中的使用越来越广泛.而图片的传输和加载往往是网页性能的瓶颈之一.为了解决这一问题,图片Base64编码与解码技术应运而生.本文将介绍图片Base64相互转换的 ...

  6. 常用SQL语句备查

    查询表中某一列是否有重复值 SELECT bizType, COUNT(bizType) FROM Res GROUP BY bizType HAVING COUNT(bizType) > 1 ...

  7. 国内如何快速访问GitHub

    1.国内如何快速访问gibhub -FQ的方法无非就是用软件,这种就不介绍了 -本次介绍的是修改本地系统主机hosts文件,绕过国内dns解析,达到快速访问github 打开https://tool. ...

  8. jq中的正则

    正则匹配表达式 \w \s \d \b . 匹配除换行符以外的任意字符 \w 匹配字母或数字或下划线或汉字 等价于 '[A-Za-z0-9_]'. \s 匹配任意的空白符 \d 匹配数字 \b 匹配单 ...

  9. 记一次 .NET某设备监控自动化系统 CPU爆高分析

    一:背景 1. 讲故事 先说一下题外话,一个监控别人系统运行状态的程序,结果自己出问题了,有时候想一想还是挺讽刺的,哈哈,开个玩笑,我们回到正题,前些天有位朋友找到我,说他们的系统会偶发性CPU爆高, ...

  10. 如何避免MYSQL主从延迟带来的读写问题?

    在MYSQL 部署架构选型上,许多公司都会用到主从读写分离的架构,如下是一个一主一从的架构,主库master负责写入,从库slave进行读取. 但是既然是读写分离,必然会面临这样一个问题,当在主库上进 ...