redis源码解析-字符串

摘要

redis中string是最简单Redis类型,本文主要通过查看源代码了解string的实现原理。

简单动态字符串

redis没有使用c语言传统的字符串表示,而是自己构建了一种名为简单动态字符串(Simple dynamic string,SDS)的抽象类型,并将SDS自以为Redis的默认字符串实现。

不过redis也并不是完全没有使用c语言标准字符串,实际上c语言标准字符串的设计在用于无需对字符串进行修改的地方更简单高效,例如日志打印。

然而,当Redis需要的不仅仅是一个字符串字面量,而是可以被修改的字符串时,Redis就会使用SDS来表示字符串值,特别是Redis在存储键值对时,底层都会使用SDS实现。

SDS的定义

sds在sds.h头文件中定义,

在之前的版本中SDS的定义为:


struct sdshdr {//记录长度int len;//记录buffer中未使用的字节数量int free;//字节数组,用于保存字符串char buf[];
}

然而不知道在什么时候,已经替换为一下代码:


typedef char *sds;
struct __attribute__ ((__packed__)) sdshdr5 {unsigned char flags; /* 3 lsb of type, and 5 msb of string length */char buf[];
};
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[];
};

本人喜欢用新不用旧,也就不再去探究原版代码了,而是直接分析新版代码,毕竟对于目前个人而言,业务上并未触碰到如此紧凑到需要真正了解底层实现的地步,所以目前是以学习源码思维为出发点,如果想要看原版代码解析的请谅解。

按照其可保存的长度分为了5种结构体,分别是sdshdr5,sdshdr8,sdshdr16,sdshdr32,sdshdr64。

sdshdr5在实际代码中并没有被使用。

其中attribute ((packed))作用是取消编译阶段的内存优化对齐功能。 题外话:packed或者__attribute((packed))关键字的作用就是用来打包数据的时候以1来对齐,比如说用来修饰结构体或者联合体的时候,那么这些成员之间就没有间隙(gaps)了。如果没有加,那么这样结构体或者联合体就会以他的自然对齐方式来对齐。比如某CPU架构的编译器默认对齐方式是4, int的size也是4,char的size是1,那么类似

typedef __packed struck test_s
{
char a;
int b;
}test_t;

这样定义的结构体的size就是8个字节了。 如果加上packed,size就会变成5个字节,中间是没有gaps的。

这个很重要,redis源码中不是直接对sdshdr某一个类型操作,往往参数都是sds,而sds就是结构体中的buf,在后面的源码分析中,你可能会经常看见s[-1]这种魔法一般的操作,而按照sdshdr内存分布s[-1]就是sdshdr中flags变量,由此可以获取到该sds指向的字符串的类型。

这五个结构体中,len表示字符串的长度,alloc表示buf指针分配空间的大小,flags表示该字符串的类型(sdshdr5,sdshdr8,sdshdr16,sdshdr32,sdshdr64),是由flags的第三位表示的。

源码如下:

#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
#define SDS_TYPE_MASK 7

可以看出SDS_TYPE只占用了0,1,2,3,4五个数字,正好占用三位,我们就可以使用flags&SDS_TYPE_MASK来获取动态字符串对应的字符串类型

通过有两个函数看看字符串原理

接下来的代码中,第一个映入眼帘的函数是:

static inline size_t sdslen(const sds s) {unsigned char flags = s[-1];switch(flags&SDS_TYPE_MASK) {case SDS_TYPE_5:return SDS_TYPE_5_LEN(flags);case SDS_TYPE_8:return SDS_HDR(8,s)->len;case SDS_TYPE_16:return SDS_HDR(16,s)->len;case SDS_TYPE_32:return SDS_HDR(32,s)->len;case SDS_TYPE_64:return SDS_HDR(64,s)->len;}return 0;
}

作用是获取sds的长度,我根据结构体定义即可知道,实际上我们只需要获取到sdshdr中的len即可在O(1)的时间复杂度下获取到字符串的长度,而传统意义上的c字符串则需要遍历字节数组,直到找到\0(作为高级语言开发者,像本人这种java开发者,很难体会到这种痛苦,(_)笑)。

但是根据函数定义,我们知道sds类型实际上仅仅只是结构体sdshdr中的字节数组,怎么获取到len属性呢?这里就需要用到之前的骚操作了,查看源码会发现函数中,首先使用s[-1]去获取到flags,如果不用内存对齐,因为struct在内存中前后会因为对其空出一截,也就不知道flags坐在地址了,但是取消内存对齐后,即可直接使用s[-1]获取到flags,然后调用SDS_HDR宏定义,我们查看SDS_HDR宏定义可以发现定义为:

#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))

其中连接符##用来将两个token连接为一个token,所以当编译器完成替换后为:

SDS_HDR(8,s);
//下面是翻译
((struct sdshdr8 *)((s) - (sizeof(struct sdshdr8))))

字节数组地址减去struct的size,就能获取到结构体的首地址,然后就能通过->len直接访问到len属性,真的很骚的黑科技。

而同理,对于接下来的sdsavail函数中的SDS_HDR_VAR

//获取sds中的可用空间
static inline size_t sdsavail(const sds s) {unsigned char flags = s[-1];switch(flags&SDS_TYPE_MASK) {case SDS_TYPE_5: {return 0;}case SDS_TYPE_8: {SDS_HDR_VAR(8,s);return sh->alloc - sh->len;}case SDS_TYPE_16: {SDS_HDR_VAR(16,s);return sh->alloc - sh->len;}case SDS_TYPE_32: {SDS_HDR_VAR(32,s);return sh->alloc - sh->len;}case SDS_TYPE_64: {SDS_HDR_VAR(64,s);return sh->alloc - sh->len;}}return 0;
}

其中使用的SDS_HDR_VAR(T,s)宏定义

#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));

也被翻译成

SDS_HDR_VAR(8,s);
//下面是对应宏定义翻译的产物
struct sdshdr8 *sh = (void*)((s)-(sizeof(struct sdshdr8)));

接着就可以直接使用*sh指针访问alloclen属性。

从该函数中可以看到获取可用空间是直接使用alloc减去len,根本没有考虑\0但是在实际实现时,还是会在最后加入\0,应该还是为了兼容c标准字符串),不使用\0作为结尾有很多好处,可以存储的类型多样性就提高了。

内联函数

//获取字符串长度
static inline size_t sdslen(const sds s){/**...**/}
//获取字符串可用空间
static inline size_t sdsavail(const sds s){/**...**/}
//设置字符串长度
static inline void sdssetlen(sds s, size_t newlen){/**...**/}
//增加字符串长度
static inline void sdsinclen(sds s, size_t inc) {/**..**/}
//获取字符串已分配空间的大小
static inline size_t sdsalloc(const sds s){/**...**/}
//设置sds已分配空间的大小
static inline void sdssetalloc(sds s, size_t newlen){/**...**/}

函数定义及实现

sds sdscatfmt(sds s, char const *fmt, ...);
sds sdstrim(sds s, const char *cset);
void sdsrange(sds s, ssize_t start, ssize_t end);
void sdsupdatelen(sds s);
void sdsclear(sds s);
int sdscmp(const sds s1, const sds s2);
sds *sdssplitlen(const char *s, ssize_t len, const char *sep, int seplen, int *count);
void sdsfreesplitres(sds *tokens, int count);
void sdstolower(sds s);
void sdstoupper(sds s);
sds sdsfromlonglong(long long value);
sds sdscatrepr(sds s, const char *p, size_t len);
sds *sdssplitargs(const char *line, int *argc);
sds sdsmapchars(sds s, const char *from, const char *to, size_t setlen);
sds sdsjoin(char **argv, int argc, char *sep);
sds sdsjoinsds(sds *argv, int argc, const char *sep, size_t seplen);sds sdsMakeRoomFor(sds s, size_t addlen);
void sdsIncrLen(sds s, ssize_t incr);
sds sdsRemoveFreeSpace(sds s);
size_t sdsAllocSize(sds s);
void *sdsAllocPtr(sds s);void *sds_malloc(size_t size);
void *sds_realloc(void *ptr, size_t size);
void sds_free(void *ptr);

这当中的大部分函数都很简单,只是对zmalloc文件里面的函数,sds中inline函数,或者是sdsnewlen函数的一层简单调用,就不解释,挑几个重点的看看。

sds sdsnewlen(const void *init, size_t initlen)

实际使用时,调用sdsnewlen生成新的sdshdr,根据init指针和initlen参数来初始化sds的内容,解读放在代码注释中:

sds sdsnewlen(const void *init, size_t initlen) {void *sh;sds s;// 根据initlen获取合适的字符串长度char type = sdsReqType(initlen);//最低使用SDS_TYPE_8if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;// 获取对应的结构体长度int hdrlen = sdsHdrSize(type);unsigned char *fp; /* flags pointer. */ 此处的s_malloc其实就是zmalloc函数,只是一个别名,注意这里,会给sds多增加一个字节的空间,由后面的s[initlen] = '\0';可知,作者是为了兼容C语言的字符串类型,这样就可以直接使用printf来输出sds了,这样非常的方便sh = s_malloc(hdrlen+initlen+1);if (sh == NULL) return NULL;// 如果init == "SDS_NOINIT",那么就会把sds置为未知字符串,如果init == NULL,那么就会把sds置为空字符串if (init==SDS_NOINIT)init = NULL;else if (!init)memset(sh, 0, hdrlen+initlen+1);s = (char*)sh+hdrlen;fp = ((unsigned char*)s)-1;// 根据sds类型来初始化sds的内容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;}}// 在初始化完成后,将init的内容拷贝进sds对象中,但是init如果原来等于SDS_NOINIT,就会被置为NULL,所以sds还是一串未知的字符串if (initlen && init)memcpy(s, init, initlen);s[initlen] = '\0';return s;
}

在动态字符串的所有操作中,大部分会进行对内存的扩大和释放,所以得介绍一下sds中对内存扩大和释放的函数

sds sdsMakeRoomFor(sds s, size_t addlen)

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;//如果当前available空间的大小大于addlen的大小,那么便不作修改if (avail >= addlen) return s;len = sdslen(s);sh = (char*)s-sdsHdrSize(oldtype);newlen = (len+addlen);// 在newlen小于SDS_MAX_PREALLOC(1M),对newlen进行翻倍// 否则让newlen加上SDS_MAX_PREALLOC。if (newlen < SDS_MAX_PREALLOC)newlen *= 2;elsenewlen += 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 {//否则重新申请内存,并拷贝内容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;
}

sds sdsRemoveFreeSpace(sds s)

sds sdsRemoveFreeSpace(sds s) {void *sh, *newsh;char type, oldtype = s[-1] & SDS_TYPE_MASK;int hdrlen, oldhdrlen = sdsHdrSize(oldtype);size_t len = sdslen(s);size_t avail = sdsavail(s);sh = (char*)s-oldhdrlen;/* Return ASAP if there is no space left. */if (avail == 0) return s;/* Check what would be the minimum SDS header that is just good enough to* fit this string. */type = sdsReqType(len);hdrlen = sdsHdrSize(type);//如果没有达到更小的字节长度则只需要重新分配内存,释放掉多余的内存if (oldtype==type || type > SDS_TYPE_8) {newsh = s_realloc(sh, oldhdrlen+len+1);if (newsh == NULL) return NULL;s = (char*)newsh+oldhdrlen;} else {//否则修改type,复制内容newsh = s_malloc(hdrlen+len+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, len);return s;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/22037.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

JVM学习-监控工具(一)

使用数据说明问题&#xff0c;使用知识分析问题&#xff0c;使用工具处理问题 无监控&#xff0c;不调优&#xff01; 命令行工具 在JDK安装目录下&#xff0c;可以查看到相应的命令行工具&#xff0c;如下图 jps(Java Process Status) 显示指定系统内所有的Hotpot虚拟机…

【自然语言处理】文本情感分析

文本情感分析 1 任务目标 1.1 案例简介 情感分析旨在挖掘文本中的主观信息&#xff0c;它是自然语言处理中的经典任务。在本次任务中&#xff0c;我们将在影评文本数据集&#xff08;Rotten Tomato&#xff09;上进行情感分析&#xff0c;通过实现课堂讲授的模型方法&#x…

OpenStack无效数据清空脚本

​​​​​​​介绍 在以openstack为底层开发的一些项目中&#xff0c;常常会遇到项目中数据与openstack数据不同步的问题&#xff0c;为了简化清空无效数据的繁琐&#xff0c;提供以下脚本便于运维操作。 环境变量 [rootcloud ~]# cat admin.sh export OS_USERNAMEadmin ex…

蓝图collapseNodes很有用

学到了&#xff0c;选中N个节点后&#xff0c;再右键collapseNode&#xff0c;可以使代码很清晰&#xff0c;双击后可以看到相应的代码&#xff0c;具有层次感。

【python科学文献计量】关于中国知网检索策略的验证,以事故伤害严重程度检索为例

关于中国知网检索策略的验证,以事故伤害严重程度检索为例 1 背景2 文献下载3 数据处理1 背景 由于要进行相关研究内容的综述,需要了解当前我国对于事故伤害严重程度的研究现状,采用国内较为知名的检索网站(中国知网)进行文献数据集检索 由于最近知网出bug,检索的结果在…

【最新鸿蒙应用开发】——使用axios完成手机号注册业务

使用Axios请求实现目标效果图&#xff1a; 短信验证码登录 校验图形验证码&#xff0c;校验通过 发送短信验证码到用户手机上&#xff0c;可通过在线 WebSocket查看&#xff1a;wss://guardian-api.itheima.net/verifyCode 根据 手机号 短信验证码 实现登录 更新图形验证码…

Python环境集成:全方位探索与实战指南

Python环境集成&#xff1a;全方位探索与实战指南 在软件开发领域&#xff0c;Python环境的集成是一项至关重要的任务。它涉及到多个组件的协同工作&#xff0c;以确保Python代码能够顺利运行。本文将从四个方面、五个方面、六个方面和七个方面对Python环境集成进行深入剖析&a…

数据结构设计算法以比较链串S1和链串S2的大小,若S1 < S2,返回-1;若S1 = S2,返回0;否则返回1。

可以使用以下算法来比较两个链串的大小&#xff1a; 创建两个指针&#xff0c;一个指向链串S1的头部&#xff0c;一个指向链串S2的头部。依次比较两个指针指向的节点的值&#xff0c;如果相等&#xff0c;则继续比较下一个节点。如果两个节点的值不相等&#xff0c;则根据节点…

基于React的SSG静态站点渲染方案

基于React的SSG静态站点渲染方案 静态站点生成SSG - Static Site Generation是一种在构建时生成静态HTML等文件资源的方法&#xff0c;其可以完全不需要服务端的运行&#xff0c;通过预先生成静态文件&#xff0c;实现快速的内容加载和高度的安全性。由于其生成的是纯静态资源…

日本指数实时API接口

日本 指数 实时API接口 # Restful API https://tsanghi.com/api/fin/index/JPN/realtime?token{token}&ticker{ticker}指定指数代码&#xff0c;获取该指数的实时行情&#xff08;开、高、低、收、量&#xff09;。 更新周期&#xff1a;实时。 请求方式&#xff1a;GET。…

CV每日论文--2024.6.4

1、Mixed Diffusion for 3D Indoor Scene Synthesis 中文 标题&#xff1a;用于 3D 室内场景合成的混合扩散 简介&#xff1a;这篇论文提出了一种名为MiDiffusion的混合离散-连续扩散模型,用于从给定的房间类型、平面图和可能存在的物体中合成逼真的3D室内场景。 作者指出,该…

【Unity实战篇 】 | Unity实现UGUI颜色渐变,支持透明渐变

前言 【Unity实战篇 】 | Unity实现UGUI颜色渐变&#xff0c;支持透明渐变一、双层颜色渐变1.1 组件属性面板1.2 效果及代码 二、多层颜色渐变2.1 组件属性面板2.2 效果及代码 总结 前言 在Unity中UGUI的实现图片和文字颜色渐变效果是一个很常见的需求。下面就来看一下颜色渐变…

机器学习中的集成学习

&#x1f4ac;内容概要 1 集成学习概述及主要研究领域 2 简单集成技术  2.1 投票法  2.2 平均法  2.3 加权平均 3 高级集成技术  3.1 Bagging  3.2 Boosting  3.3 Bagging vs Boosting 4 基于Bagging和Boosting的机器学习算法  4.1 sklearn中的Bagging算法  4.2 sklea…

python 深浅拷贝

浅拷贝 copy函数就是浅拷贝 copy函数是浅拷贝&#xff0c;只对可变类型的第一层对象进行拷贝 对拷贝的对象开辟新的内存空间进行存储&#xff0c;子对象不会开辟新的空间 list1 [1, 2, 3] list2 [a, list1] list3 list2.copy()print(id(list1)) print(id(list2)) …

Layout软件AD中关于铺铜的技巧

Layout软件AD中关于铺铜的技巧 目录 一.铜的连接方式: 二.关于铜的编辑: 三.Shelve的使用:

探索Linux世界的钥匙:Bash命令详解

标题&#xff1a;探索Linux世界的钥匙&#xff1a;Bash命令详解 引言&#xff1a; 在Linux的世界里&#xff0c;Bash&#xff08;Bourne Again Shell&#xff09;无疑是每个用户和系统管理员的得力助手。作为Linux系统中最常用的shell之一&#xff0c;Bash提供了丰富的命令和强…

1961. 检查字符串是否为数组前缀 - 力扣

1. 题目 给你一个字符串 s 和一个字符串数组 words &#xff0c;请你判断 s 是否为 words 的 前缀字符串 。 字符串 s 要成为 words 的 前缀字符串 &#xff0c;需要满足&#xff1a;s 可以由 words 中的前 k&#xff08;k 为 正数 &#xff09;个字符串按顺序相连得到&#xf…

大型语言模型的工作原理(LLM:从零学起)

目录 一、说明 二、LLM如何运作 三、预训练&#xff1a;基本模型 四、微调&#xff1a;培训助手 五、RLHF&#xff1a;从人类反馈中强化学习 六、提示工程 七、总结 一、说明 这是我们谈论LLM系列的第二篇文章。在本文中&#xff0c;我们旨在为大型语言模型 &#xff08;LLM&am…

Feign @SpringQueryMap将POJO或Map参数注释为查询参数映射

一、 Feign SpringQueryMap支持 OpenFeign QueryMap批注支持将POJO用作GET参数映射。不幸的是&#xff0c;默认的OpenFeign QueryMap注释与Spring不兼容&#xff0c;因为它缺少value属性。 Spring Cloud OpenFeign提供等效的SpringQueryMap批注&#xff0c;该批注用于将POJO或…

企业微信hook接口协议,ipad协议http,chatid转群id

chatid转群id 参数名必选类型说明uuid是String每个实例的唯一标识&#xff0c;根据uuid操作具体企业微信 请求示例 {"uuid":"3240fde0-45e2-48c0-90e8-cb098d0ebe43","chatid":"wrO9o4EAAAeR_nSlmjeX1RWrKAKxN8jQ" } 返回示例 {&…