学习分享(第二期):从源码层面看Redis的省内存设计

发布于:2024-10-24 编辑:匿名 来源:网络

每周学习分享记录于此,周一/周二发布,文章维护在Github:studeyang/leanrning-share 。回顾第《Redis 的 String 类型,原来这么占内存》文章,我们了解了SDS的底层结构,发现SDS存储了大量的元数据。

再加上全局哈希表的实现,Redis String类型在内存使用方面并不理想。然后在《学习分享(第1期)之Redis:巧用Hash类型节省内存》文章中,我们学习了另一种节省内存的解决方案。

使用ziplist结构的Hash类型,内存占用减少一半,效果显着。虽然我们使用的是String类型,占用内存较多,但Redis其实对SDS有节省内存的设计。

除此之外,Redis还考虑了其??他方面的内存开销。今天我们就从源码层面来看看节省内存的设计。

本文中的代码版本是6.2.4。 1、redisObject的位域定义方法我们知道redisObject是对SDS、ziplist等底层数据结构的封装。

因此,如果redisObject能够得到优化,最终能够带来节省内存的用户体验。 redisObject的结构体在源码server.h中定义,如下代码所示: 代码语言:c copy #define LRU_BITS 24typedef struct redisObject { unsigned type: 4; // 对象类型(4 位 = 0.5 字节) unsigned encoding :4;//编码(4 位 = 0.5 字节) unsigned lru:LRU_BITS;//记录应用程序最后一次访问该对象的时间(24 位 = 3 bytes) int refcount;//引用计数。

等于0时,表示可以被垃圾回收(32位=4字节) void *ptr;//指向底层实际数据存储结构,如:sds等(8字节)} robj; type、encoding、lru、refcount都是redisObject的元数据。 redisObject的结构如下图所示。

从代码中我们可以看到,type、encoding、lru这三个变量后面有一个冒号,后面跟着一个值,表示元数据占用的位数。这种使用冒号和值来定义变量的方法实际上是C语言中的位域定义方法,可以用来有效节省内存开销。

这种方法比较适合的场景是,当一个变量无法占据某个数据类型的所有位时,可以使用位域定义方法,将某个数据类型中的位(32位)划分为多个(3个)位域,每个位域占用一定数量的位。这样,一个数据类型的所有位都可以用来定义多个变量,从而有效节省内存开销。

另外,SDS的设计中还有内存优化设计。让我们详细看看它们。

2. SDS 的设计 Redis 3.2 版本之后,SDS 从一种数据结构变成了五种数据结构。它们是 sdshdr5、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64。

其中sdshdr5仅用于Redis中的key中。其他四种不同类型的结构头可以适应不同大小的字符串。

以 sdshdr8 为例,其结构体定义如下: 代码语言: c copy struct __attribute__ ((__packed__)) sdshdr8 { uint8_t len; /* 使用 */ uint8_t alloc; /* 排除标头和空终止符 */ unsigned char flags; /* 3 个 lsb 类型,5 个未使用的位 */ char buf[];};不知道大家有没有注意到struct和sdshdr8之间使用了__attribute__((__packed__))。这是 SDS 的节省内存设计 - 紧凑字符串。

2.1 紧字符串 什么是紧字符串?它的作用是告诉编译器在编译sdshdr8结构时不要使用字节对齐,而是以紧凑的方式分配内存。默认情况下,编译器以 8 字节对齐方式为变量分配内存。

也就是说,即使变量的大小小于 8 个字节,编译器也会为其分配 8 个字节。举个例子。

假设我定义了一个结构体st1,它有两个成员变量,类型分别是char和int,如下: 代码语言:c Copy #include int main() { struct st1 { char a; }整数b; } ts1; printf("%lu\n", sizeof(ts1)); return 0;} 我们知道char类型占用1个字节,int类型占用4个字节,但是如果你运行这段代码,你会发现打印的结果是8。这是因为默认情况下,编译器分配了8个字节空间以8字节对齐方式分配给st1结构,但这样就浪费了3个字节。

然后我使用 __attribute__ ((__packed__)) 属性重新定义结构体 st2,其中也包含两种类型的成员变量:char 和 int。代码如下: 代码语言:c copy #include int main() { struct __attribute__((packed)) st2{ char a; }整数b; } ts2; printf("%lu\n", sizeof(ts2)); return 0;} 当你运行这段代码时,你可以看到打印结果是5,这是紧凑的内存分配。

st2结构只占用5个字节的空间。另外,Redis还做了这样的优化:当保存的字符串小于等于44字节时,RedisObject中的元数据、指针和SDS都是连续的内存区域。

这种布局方法称为embstr编码。方法;当字符串大于44字节时,SDS和RedisObject分开布局。

这种布局方法称为原始编码模式。这部分代码在object.c文件中: 代码语言:c copy #define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44robj *createStringObject(const char *ptr, size_t len) { // 当字符串长度小于等于44字节时,创建嵌入字符串如果(len <= Object 部分),则 createStringObject 函数调用 createEmbeddedStringObject 函数。

这是 SDS 的第二个节省内存的设计——嵌入式字符串。在讲嵌入字符串之前,我们先来看看当len长度大于OBJ_ENCODING_EMBSTR_SIZE_LIMIT(默认为44字节)时这个普通字符串的创建过程。

2.2 RawString 普通字符串 对于createRawStringObject函数来说,在创建String类型的值时会调用createObject函数。 createObject函数主要用于创建Redis数据对象。

代码如下所示。代码语言:c copy robj *createRawStringObject(const char *ptr, size_t len) { return createObject(OBJ_STRING, sdsnewlen(ptr,len));} createObject函数有两个参数,一个用于指示要创建的数据对象的类型被创建,另一个是指向SDS对象的指针,这个指针是通过sdsnewlen函数创建的。

代码语言:c copy robj *createObject(int type, void *ptr) { // 【1】为redisObject结构体分配内存空间 robj *o = zmalloc(sizeof(*o)); //设置redisObject的类型 o->type = type; //设置redisObject的编码类型 o->encoding = OBJ_ENCODING_RAW; // 【2】将传入的指针赋值给redisObject中的指针 o->ptr = ptr; … return o;} 调用sdsnewlen函数创建SDS对象的指针后,还分配了一块SDS内存空间。接下来,createObject函数将为redisObject结构体分配内存空间,如上面的代码[1]所示。

然后将传入的指针赋值给redisObject中的指针,如上面代码[2]所示。接下来让我们看看嵌入的字符串。

2.3 EmbeddedString 嵌入字符串由上面我们知道,当保存的字符串小于等于44字节时,RedisObject中的元数据、指针和SDS都是连续的内存区域。那么createEmbeddedStringObject函数是如何将redisObject和SDS的内存区域放在一起的呢?代码语言:c copy robj *createEmbeddedStringObject(const char *ptr, size_t len) { // [1] robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr8)+len); ... if (ptr == SDS_NOINIT) sh->buf[len] = '\0'; else if (ptr) { // [2] memcpy(sh->buf,ptr,len); sh->buf[len] = '\0'; } else { memset(sh->buf,0,len); } return o;} 首先,createEmbeddedStringObject函数会分配一块连续的内存空间。

该内存空间的大小等于redisObject结构体+SDS结构体头的大小。 sdshdr8 的大小 + 字符串大小的总和,加上 1 字节终止字符“\0”。

这部分代码如上面的[1]所示。首先分配连续的内存空间以避免内存碎片。

然后,createEmbeddedStringObject函数会将参数传入的指针ptr指向的字符串复制到SDS结构体中的字符数组中,并在数组末尾添加结束字符。这部分代码如上面的[2]所示。

好了,以上就是Redis在设计SDS结构来节省内存方面的两个优化点。不过,除了嵌入字符串之外,Redis还设计了压缩列表,这也是一种紧凑的内存数据结构。

下面我们就来了解一下。下载其设计理念。

3.压缩列表的设计为了方便理解压缩列表的设计和实现,我们首先看一下它的创建函数ziplistNew。这部分代码在ziplist.c文件中,如下所示: 代码语言:c copy unsigned char *ziplistNew (void) { // 初始分配大小 unsigned int bytes = ZIPLIST_HEADER_SIZE+ZIPLIST_END_SIZE;无符号字符 *zl = zmalloc(字节); … // 将列表末尾设置为 ZIP_END zl[bytes-1] = ZIP_END; return zl;} 可以看到,ziplistNew函数的逻辑很简单,就是创建一个大小为ZIPLIST_HEADER_SIZE和ZIPLIST_END_SIZE之和的连续内存空间,然后将连续空间的最后一个字节赋给ZIP_END ,表示列表的末尾。

另外,ziplist.c文件中还定义了ZIPLIST_HEADER_SIZE、ZIPLIST_END_SIZE和ZIP_END的值,它们分别代表ziplist的列表头大小、列表尾大小和列表尾字节内容,如下所示。代码语言:c copy //ziplist列表头大小 #define ZIPLIST_HEADER_SIZE (sizeof(uint32_t)*2 + sizeof(uint16_t))//ziplist列表尾部大小 #define ZIPLIST_END_SIZE (sizeof(uint8_t))//ziplist列表尾部大小 字节内容#define ZIP_END 列表头包括两个32位整数和一个16位整数,分别表示压缩列表的总字节数zlbytes、列表最后一个元素距列表头zltail的偏移量、列表中的元素数量 Number zllen;列表的末尾包含一个8位整数,表示列表的末尾。

执行ziplistNew函数创建ziplist后,内存布局如下图所示。注意,此时ziplist中还没有实际数据,所以图中没有画出来。

然后,当我们向ziplist中插入数据时,完整的内存布局如下图所示。ziplist条目包括三部分,即前一项的长度(prevlen)、当前项的长度信息的编码结果(encoding)、当前项的实际数据(data)。

当我们向ziplist中插入数据时,ziplist会根据数据是字符串还是整数以及它们的大小对其进行不同的编码。这种根据数据大小进行相应编码的设计思想,正是Redis为了节省内存而采用的。

代码语言: c copy unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) { ... /* 写入条目 */ p += zipStorePrevEntryLength(p,prevlen); p += zipStoreEntryEncoding(p,编码,slen); if (ZIP_IS_STR(编码)) { memcpy(p,s,slen); } else { zipSaveInteger(p,值,编码); } ZIPLIST_INCR_LENGTH(zl,1); return zl;} 这里的源码下面会提到。为了描述方便,这里标记为【源代码A】。

此外,前一项的长度将被记录在每个列表项条目中。由于每个列表项的长度不同,Redis会根据数据长度选择不同大小的字节来记录prevlen。

3.1 使用不同大小的字节来记录prevlen。例如,假设我们使用4个字节来记录prevlen。

如果前面的列表项只是一个5字节的字符串“redis”,那么我们用1个字节(8位)就可以表示一个0~byte长度的字符串。此时prevlen用了4个字节来记录,浪费了3个字节。

我们来看看Redis是如何根据数据长度选择不同大小的字节来记录prevlen的。从上面【源码A】的__ziplistInsert函数可以看到,ziplist在对prevlen进行编码时,会首先调用zipStorePrevEntryLength函数。

函数代码如下: 代码语言: c copy unsigned int zipStorePrevEntryLength(unsigned char *p, unsigned int len) { if (p == NULL) { return (len < ZIP_BIG_PREVLEN) ? 1 : sizeof(uint32_t) + 1; } else { //判断prevlen的长度是否小于ZIP_BIG_PREVLEN if (len < ZIP_BIG_PREVLEN) { //如果小于bytes,则返回prevlen为1字节 p[0] = len;返回1; } else { //否则,调用zipStorePrevEntryLengthLarge进行编码 return zipStorePrevEntryLengthLarge(p,len);可以看到, zipStorePrevEntryLength 函数会判断前一个列表项是否小于 ZIP_BIG_PREVLEN (ZIP_BIG_PREVLEN 的值为)。如果是,则prevlen用1个字节表示;否则,zipStorePrevEntryLength 函数调用 zipStorePrevEntryLengthLarge 函数进行进一步编码。

zipStorePrevEntryLengthLarge函数首先会将prevlen的第1个字节设置为 ,然后使用内存复制函数memcpy将前一个列表项的长度值复制到prevlen的第2到第5个字节。最后,zipStorePrevEntryLengthLarge 函数返回 prevlen 的大小,即 5 个字节。

代码语言:c copy int zipStorePrevEntryLengthLarge(unsigned char *p, unsigned int len) { uint32_t u32; if (p != NULL) { //设置prevlen的第一个字节为ZIP_BIG_PREVLEN,即p[0] = ZIP_BIG_PREVLEN; u32 = 长度; //将前一个列表项的长度值复制到prevlen的第2到第5个字节,其中sizeof(u32)的值为4 memcpy(p,&u32,sizeof(u32)); ... } //返回prevlen的大小,即5字节 return 1 + sizeof(uint32_t);} 好了,了解了prevlen采用1字节和5字节两种编码方式后,我们来了解一下编码方式。 。

3.2 使用不同大小的字节来记录编码。我们回到上面的__ziplistInsert函数,也就是【源代码A】。

我们可以看到,执行完zipStorePrevEntryLength函数逻辑后,会立即调用zipStoreEntryEncoding函数。在zipStoreEntryEncoding函数中,ziplist对整数和字符串使用不同字节长度的编码结果。

代码语言:c copy unsigned int zipStoreEncoding(unsigned char *p, unsigned char编码, unsigned int rawlen) { //默认编码结果为1字节 unsigned char len = 1; //如果是字符串数据 if (ZIP_IS_STR(encoding )) { //如果字符串长度小于等于63字节(十六进制为0x3f) if (rawlen <= 0x3f) { //默认编码结果为1字节 if (!p) 返回 len; ... } //字符串长度小于等于3个字节(十六进制为0x3fff) else if (rawlen <= 0x3fff) { //编码结果为2个字节 len += 1; if (!p) 返回 len; ... } //字符串长度大于3个字节 else { //编码结果为5个字节 len += 4; if (!p) 返回 len; ... } } else { /* 如果数据为整数,则编码结果为 1 个字节 */ if (!p) return len; ... }} 可以看出,当数据是不同长度的字符串或者整数时,编码结果的长度len是不同的。简而言之,对于不同长度的数据,采用元数据信息prevlen和不同字节大小的编码来记录。

这种方法可以有效节省内存开销。

学习分享(第二期):从源码层面看Redis的省内存设计

站长声明

版权声明:本文内容由互联网用户自发贡献,本站不拥有所有权,不承担相关法律责任。如果发现本站有涉嫌抄袭的内容,欢迎发送邮件 举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。

标签:

相关文章

  • vivo的无字书是空的还是有深度?

    vivo的无字书是空的还是有深度?

    今年1月25日,当vivo作为微信朋友圈前四大广告商中唯一一家智能手机厂商登陆微信朋友圈时,那热闹的场景还历历在目。 不到半年的时间,vivo在广告界再次掀起轩然大波——这一次,vivo将同样的四图广告搬到了《人民日报》。 5月13日《人民日报》,vivo打包了四页。 然而,其中

    06-17

  • 企云方获得数千万元Pre-A轮融资,戈壁创投领投

    企云方获得数千万元Pre-A轮融资,戈壁创投领投

    企云方科技今日宣布完成数千万元Pre-A轮融资。 本轮融资由戈壁创投领投,用友产业基金跟投。 企云方科技于2016年在美国硅谷成立研发团队,致力于开发高效、灵活、易用、高性价比的下一代绩效管理平台。 据了解,本轮融资将用于推动产品研发优化,以及企业营销和技术团队的扩充

    06-17

  • 中国“乡土味”短剧在美国火爆

    中国“乡土味”短剧在美国火爆

    11月初,真人短剧结合游戏模式《完蛋!我被美女包围了!》疯狂霸占Steam排行榜后,带有浓郁中国基因的《Boss》短剧在美国也疯狂地杀人。 近日,一款名为ReelShort的短剧应用跻身Apple Store前三,甚至超过称霸榜单多年的TikTok,升至娱乐榜榜首。 打开这个应用后,你会发现各

    06-17

  • 难道小米造出了连苹果都无法量产的AirPower?

    难道小米造出了连苹果都无法量产的AirPower?

    在小米的发布会上,它可能会像小米11 Ultra和MIX FOLD一样受到关注。 小米发布会上半场,有一款新产品引发热议。 它既不是安卓王小米11 Pro,也不是安卓轻小米11 Ultra,而是Keynote上“路过”的小米多线圈无线快充板。 毫不夸张地说,小米的多线圈无线快充板抢了几乎所有新品

    06-21

  • KLA FY2Q20:受益EUV,逻辑IC所需设备占比提升

    KLA FY2Q20:受益EUV,逻辑IC所需设备占比提升

    祥平科技【国盛郑振祥团队】经营情况:FY2Q20实现营业收入15.09亿美元,同比增长6.79%,环比增长12.31%;毛利率为60.8%;净利润3.81亿美元,同比增长3.10%,环比增长9.86%;营业利润率为34.8%,毛利率为60.8%。 其中,半导体控制工艺营收达12.48亿美元,占比约83%,环比增长7

    06-06

  • 乌鲁木齐高新区设立2亿引导基金

    乌鲁木齐高新区设立2亿引导基金

    据投资界2月14日消息,高新区(新城区)普惠创业社区引导基金合伙企业近日成立并获得营业执照,标志着高新区(新城区)创业社区引导基金正式启动。 该基金总规模2亿元,主要用于解决困扰创业者的融资难、融资贵问题,引导创业多元化、特色化、专业化发展。 该合伙企业由新疆火

    06-18

  • 鲸鱼机器人完成Pre-B轮融资

    鲸鱼机器人完成Pre-B轮融资

    投资社区(ID:pedaily)据2月26日消息,青少年人工智能教育服务商鲸鱼机器人完成了南虹领投的万元Pre-B轮融资首都。 这是“鲸鱼机器人”自2016年成立以来完成的第五轮融资。 本轮融资将主要用于青少年人工智能技术的深度发展,同时也帮助公司应对青年人工智能技术的快速发展

    06-17

  • 开发投资800万欧元!西班牙两大能源巨头加速推广光伏制氢技术

    开发投资800万欧元!西班牙两大能源巨头加速推广光伏制氢技术

    西班牙两大能源巨头石油公司Repsol和Enags正计划在雷普索尔位于Puertollano的工业综合体建设基于光电催化的电解槽。 该设施将接收直接的太阳辐射,并通过光敏材料产生电荷,将水分子分解为氢气和氧气。 经过近十年的研究工作,来自西班牙两大能源公司雷普索尔和Enags的研究团

    06-08

  • 【全球财经24小时】2024年3月8日投融资事件汇总及详情

    【全球财经24小时】2024年3月8日投融资事件汇总及详情

    欢迎订阅《全球财经24小时》系列文章,动起你的小手指,帮助我们更好更快地获取资讯给你~ 点击此处输入表格摘要。 今日全球市场共发生19起投资披露事件,其中境内11起,境外8起。 其中,国内先进制造业2例,汽车交通运输业2例,传统制造业3例,企业服务业1例,游戏业1例,金融

    06-18

  • 致力于国内畜牧流通行业转型升级,耀明物流获数千万元Pre-A轮投资

    致力于国内畜牧流通行业转型升级,耀明物流获数千万元Pre-A轮投资

    投资界动态(微信ID:pedialy),国内畜牧流通企业江苏耀明物流专业畜牧业科技物流解决方案提供商,获得数千万元Pre-A轮投资。 本轮投资方为国投物流与供应链产业发展基金。 本轮融资后,耀明物流将进一步加速国内畜牧流通行业转型升级,打造健康安全的特色智慧农牧供应链服

    06-17

  • 新项目NO.34|独家专访美克盛能源副总裁胡金双:数字能源与储能安全领域的“双独角兽”

    新项目NO.34|独家专访美克盛能源副总裁胡金双:数字能源与储能安全领域的“双独角兽”

    随着人类社会的电气化、信息化,我们在享受人类社会所取得的成就的同时科学技术,我们也在享受科学技术的成果。 承担温室效应和气候变化的成本。 大自然向人类发出了预警。 在“双碳”目标背景下,绿色能源如何惠及大众?如何创造更安全的绿色能源生活?本期《看见新项目》专

    06-18

  • 新闻丨“用友智行”获数千万元A轮融资,万达领投

    新闻丨“用友智行”获数千万元A轮融资,万达领投

    据投资界6月23日消息,中国两轮电动车换电运营商用友智行近日宣布完成融资完成数千万元A轮融资。 本轮融资由万达投资领投,逍遥资本跟投。 据悉,本轮融资后,用友智行估值达5亿元,融资将用于拓展全国重点城市的换电业务。 用友智行成立于2006年,主要研发符合“新国标”的两

    06-18