学习分享(第二期):从源码层面看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的省内存设计

站长声明

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

标签:

相关文章

  • 美光科技任命朱文菊为美光半导体(西安)有限责任公司副总裁

    美光科技任命朱文菊为美光半导体(西安)有限责任公司副总裁

    美光科技任命 Stella Zhu 为美光半导体(西安)有限公司副总裁。 2020 年 8 月 17 日,中国西安 — 美光科技股份有限公司(纳斯达克股票代码:MU)今天宣布任命 Stella朱(Stella Zhu))担任美光半导体(西安)有限公司副总裁,负责美光西安工厂的整体运营。 该工厂成立于20

    06-06

  • 首次发布 -光芯片公司“斑岩光子”完成A轮融资,超过10家VC共同投资

    首次发布 -光芯片公司“斑岩光子”完成A轮融资,超过10家VC共同投资

    投资界(ID:pedaily)8月25日消息,中国领先的高速EML光芯片提供商“斑岩光子”完成A轮融资本轮融资由卓源资本、利哲创投、国盛资产、德林资本、元和控股、合创致远、星湖控股、海南融腾、纳芯创投、联智投资、东莞伟卓等十余家半导体及实业顶级投资方投资。 投资机构联合追

    06-17

  • 特斯拉全球裁员10%以上,迎接新增长周期

    特斯拉全球裁员10%以上,迎接新增长周期

    特斯拉简介及里程碑回顾 特斯拉公司成立于2007年,是一家电动汽车和清洁能源公司,总部位于美国加利福尼亚州。 特斯拉以其创新的电动汽车和能源解决方案而闻名,致力于推动全球向可持续能源转型。 过去两年,特斯拉实现了多个重要里程碑: ● 2016年,特斯拉推出最新车型Mode

    06-18

  • 红细胞药物研发公司西湖生物医药宣布完成近亿元Pre-A+轮融资

    红细胞药物研发公司西湖生物医药宣布完成近亿元Pre-A+轮融资

    投资界(ID:pedaily)1月23日消息,西湖生物医药(杭州)有限公司西湖生物医药股份有限公司(简称:西湖生物医药/Westlake Therapeutics)正式宣布完成近亿元Pre-A+轮融资,由辰德资本领投,红杉资本中国基金跟投。 本次融资是西湖生物医药继去年6月Pre-A轮融资后的又一轮战

    06-18

  • 晶圆级扇出先进封装企业“晶通科技”获数千万元A轮融资

    晶圆级扇出先进封装企业“晶通科技”获数千万元A轮融资

    投资界(ID:pedaily)9月20日消息,杭州晶通科技有限公司(以下简称“晶通科技”)晶通科技)近日宣布完成数千万元A轮融资,本轮融资由水木梧桐创投、天冲资本、春阳资本共同参与,独木资本作为公司的长期投资人。 任期独家融资顾问。 资金将主要用于晶圆级扇出和chiplet产品

    06-18

  • 迷惑人类行为:让纸人写稿、听虚拟偶像和破音

    迷惑人类行为:让纸人写稿、听虚拟偶像和破音

    你有没有感觉微博热搜上的不知名名字越来越多?你有没有想过你的追星小伙伴们都没听说过明星?你是否觉得电视上越来越多的面孔你认不出来了?如果你对这三个问题的回答都是“是”,那么你可能不会观看选秀或追求偶像。 毕竟现在有很多追星粉丝,有一堵墙、二堵墙、三堵墙、四

    06-21

  • 中国手机供应链正在逃离印度

    中国手机供应链正在逃离印度

    10月11日,有消息称,印度因涉嫌洗钱逮捕了四名与vivo相关的人员,其中一名是vivo的中国员工。 vivo回应称,“vivo严格遵守印度当地法律法规,我们正在密切关注近期的调查,并将采取一切可行的法律措施予以回应。 ”近一两年来,印度频繁针对中国企业的搜查行动至今仍没有结束

    06-18

  • 微信应用和iPhone 7能为3D音频带来什么?

    微信应用和iPhone 7能为3D音频带来什么?

    在贵州省黔南布依族自治州平塘县“大窝荡”洼地,FAST“米径球面日冕主动反射球面射电望远镜”,经历了五年多的建设和反复调试。 终于竣工并投入使用,迈向试运营阶段。 9月24日,FAST探测核心部件馈源模块首次进行大规模移动测试,为完成后的观测任务做最后准备。 9月25日,

    06-18

  • 意法半导体:SiC晶圆产能增长10倍

    意法半导体:SiC晶圆产能增长10倍

    欧洲IDM厂商意法半导体(ST)总裁兼首席执行官Jean-Marc Chery发布最新年度市场展望。 预计2020年全球芯片短缺的情况将逐渐改善,至少要到上半年才能恢复到“正常”水平。 意法半导体看好三大商机:智慧出行、电力能源、物联网和5G。 它计划在未来四年内大幅提高晶圆产能,并

    06-08

  • 鹰牌药业完成4亿元D+轮融资,加速造福更多卵巢癌患者 患者

    鹰牌药业完成4亿元D+轮融资,加速造福更多卵巢癌患者 患者

    投资界(ID:pedaily)4月19日消息,南京鹰牌药业股份有限公司(以下简称鹰牌药业)专注于肿瘤合成致死机制的创新药物研发公司“鹰派药业”)宣布成功完成4亿元D+轮融资。 本轮融资由高特嘉投资、熙诚金睿联合领投。 扬州国金集团和顾屿南歌参与了本次投资。 老股东礼来亚洲基

    06-17

  • 首次发布 -星逻智智能获数千万元A+轮融资

    首次发布 -星逻智智能获数千万元A+轮融资

    投资社区(ID:pedaily)据3月30日消息,无人机自动化公司星逻智智能今日宣布完成数千万元A+轮融资的融资。 本次融资由中关村发展集团启航创新投资基金(以下简称“启航投资”)领投,老股东奥文创投、常春藤资本、愿景资本跟投。 本轮融资将主要用于无人机AI研发,加速提升无

    06-17

  • 理想汽车6月交付7713辆 同比增长320.6%

    理想汽车6月交付7713辆 同比增长320.6%

    理想汽车6月交付7713辆,同比增长320.6%,5月环比增长78.4%,创下单月交付量新纪录。

    06-17