玮科网站建设,推广链接赚钱,短视频app成品搭建源码免费,做情人节网站目录
前言
内存友好的数据结构
SDS 的内存友好设计
redisObject 结构体与位域定义方法
嵌入式字符串
压缩列表和整数集合的设计
节省内存的数据访问 前言 Redis 是内存数据库#xff0c;所以#xff0c;高效使用内存对 Redis 的实现来说非常重要而实际上#xff0c;R…目录
前言
内存友好的数据结构
SDS 的内存友好设计
redisObject 结构体与位域定义方法
嵌入式字符串
压缩列表和整数集合的设计
节省内存的数据访问 前言 Redis 是内存数据库所以高效使用内存对 Redis 的实现来说非常重要而实际上Redis 主要是通过两大方面的技术来提升内存使用效率的分别是数据结构的优化设计与使用以及内存数据按一定规则淘汰关于内存数据按规则淘汰这是通过 Redis 内存替换策略实现的也就是将很少使用的数据从内存中淘汰从而把有限的内存空间用于保存会被频繁访问的数据这部分的设计与实现主要和内存替换策略有关Redis 数据结构在面向内存使用效率方面的优化其中包括两方面的设计思路 一是内存友好的数据结构设计二是内存友好的数据使用方式这两方面的设计思路和实现方法是具有通用性的当你在设计系统软件时如果需要对内存使用精打细算以便节省内存开销这两种设计方法和实现考虑就非常值得学习和掌握 内存友好的数据结构 首先要知道在 Redis 中有三种数据结构针对内存使用效率做了设计优化分别是 简单动态字符串SDS压缩列表ziplist整数集合intset SDS 的内存友好设计 在第 2 讲中就已经介绍过 SDS 的结构设计这里先做个简单的回顾SDS 设计了不同类型的结构头包括 sdshdr8、sdshdr16、sdshdr32 和 sdshdr64这些不同类型的结构头可以适配不同大小的字符串从而避免了内存浪费不过SDS 除了使用精巧设计的结构头外在保存较小字符串时其实还使用了嵌入式字符串的设计方法这种方法避免了给字符串分配额外的空间而是可以让字符串直接保存在Redis 的基本数据对象结构体中所以这也就是说要想理解嵌入式字符串的设计与实现就需要先来了解下Redis 使用的基本数据对象结构体 redisObject 是什么样的 redisObject 结构体与位域定义方法 redisObject 结构体是在 server.h 文件中定义的主要功能是用来保存键值对中的值这个结构一共定义了 4 个元数据和一个指针 typeredisObject 的数据类型是应用程序在 Redis 中保存的数据类型包括 String、List、Hash 等encodingredisObject 的编码类型是 Redis 内部实现各种数据类型所用的数据结构lruredisObject 的 LRU 时间refcountredisObject 的引用计数ptr指向值的指针下面的代码展示了 redisObject 结构体的定义 从代码中可以看到在 type、encoding 和 lru 三个变量后面都有一个冒号并紧跟着一个数值表示该元数据占用的比特数其中type 和 encoding 分别占 4bits而 lru 占用的比特数是由 server.h 中的宏定义 LRU_BITS 决定的它的默认值是 24bits如下所示 而这里要掌握的就是这种变量后使用冒号和数值的定义方法这实际上是 C 语言中的位域定义方法可以用来有效地节省内存开销这种方法比较适用的场景是当一个变量占用不了一个数据类型的所有 bits 时就可以使用位域定义方法把一个数据类型中的 bits划分成多个位域每个位域占一定的 bit 数这样一来一个数据类型的所有 bits 就可以定义多个变量了从而也就有效节省了内存开销此外你可能还会发现对于 type、encoding 和 lru 三个变量来说它们的数据类型都是unsigned已知一个 unsigned 类型是 4 字节但这三个变量是分别占用了一个unsigned 类型 4 字节中的 4bits、4bits 和 24bits因此相较于三个变量每个变量用一个 4 字节的 unsigned 类型定义来说使用位域定义方法可以让三个变量只用 4 字节最后就能节省 8 字节的开销所以当你在设计开发内存敏感型的软件时就可以把这种位域定义方法使用起来了解了 redisObject 结构体和它使用的位域定义方法以后再来看嵌入式字符串是如何实现的 嵌入式字符串 前面说过SDS 在保存比较小的字符串时会使用嵌入式字符串的设计方法将字符串直接保存在 redisObject 结构体中然后在 redisObject 结构体中存在一个指向值的指针ptr而一般来说这个 ptr 指针会指向值的数据结构这里就以创建一个 String 类型的值为例Redis 会调用 createStringObject 函数来创建相应的 redisObject而这个 redisObject 中的 ptr 指针就会指向 SDS 数据结构如下图所示 在 Redis 源码中createStringObject 函数会根据要创建的字符串的长度决定具体调用哪个函数来完成创建那么针对这个 createStringObject 函数来说它的参数是字符串 ptr 和字符串长度 len当len 的长度大于 OBJ_ENCODING_EMBSTR_SIZE_LIMIT 这个宏定义时createStringObject函数会调用 createRawStringObject 函数否则就调用 createEmbeddedStringObject 函数而在分析的 Redis 5.0.8 源码版本中这个 OBJ_ENCODING_EMBSTR_SIZE_LIMIT默认定义为 44 字节这部分代码如下所示 现在就来分析一下 createStringObject 函数的源码实现以此了解大于 44 字节的普通字符串和小于等于 44 字节的嵌入式字符串分别是如何创建的首先对于 createRawStringObject 函数来说它在创建 String 类型的值的时候会调用createObject 函数 补充createObject 函数主要是用来创建 Redis 的数据对象的因为 Redis 的数据对象有很多类型比如 String、List、Hash 等所以在 createObject 函数的两个参数中有一个就是用来表示所要创建的数据对象类型而另一个是指向数据对象的指针然后createRawStringObject 函数在调用 createObject 函数时会传递OBJ_STRING 类型表示要创建 String 类型的对象以及传递指向 SDS 结构的指针如以下代码所示这里需要注意的是指向 SDS 结构的指针是由 sdsnewlen 函数返回的而 sdsnewlen 函数正是用来创建 SDS 结构的 最后再来进一步看下 createObject 函数这个函数会把参数中传入的、指向 SDS 结构体的指针直接赋值给 redisObject 中的 ptr这部分的代码如下所示 为理解普通字符串创建方法看下图 这也就是说在创建普通字符串时Redis 需要分别给 redisObject 和 SDS 分别分配一次内存这样就既带来了内存分配开销同时也会导致内存碎片因此当字符串小于等于 44 字节时Redis 就使用了嵌入式字符串的创建方法以此减少内存分配和内存碎片而这个创建方法就是由前面提到的 createEmbeddedStringObject 函数来完成的该函数会使用一块连续的内存空间来同时保存 redisObject 和 SDS 结构这样一来内存分配只有一次而且也避免了内存碎片createEmbeddedStringObject 函数的原型定义如下它的参数就是从 createStringObject函数参数中获得的字符串指针 ptr以及字符串长度 len 那么下面就来具体看看createEmbeddedStringObject 函数是如何把 redisObject和 SDS 放置在一起的首先createEmbeddedStringObject 函数会分配一块连续的内存空间这块内存空间的大小等于 redisObject 结构体的大小、SDS 结构头 sdshdr8 的大小和字符串大小的总和并且再加上 1 字节注意这里最后的 1 字节是 SDS 中加在字符串最后的结束字符“\0”这块连续内存空间的分配情况如以下代码所示 可以参考下图其中展示了这块内存空间的布局 那么 createEmbeddedStringObject 函数在分配了内存空间之后就会创建 SDS 结构的指针 sh并把 sh 指向这块连续空间中 SDS 结构头所在的位置下面的代码显示了这步操作其中o 是 redisObject 结构体的变量o1 表示将内存地址从变量 o 开始移动一段距离而移动的距离等于 redisObject 这个结构体的大小 经过这步操作后sh 指向的位置就如下图所示 紧接着createEmbeddedStringObject 函数会把 redisObject 中的指针 ptr指向 SDS 结构中的字符数组如以下代码所示其中 sh 是刚才介绍的指向 SDS 结构的指针属于 sdshdr8 类型而sh1 表示把内存地址从 sh 起始地址开始移动一定的大小移动的距离等于 sdshdr8 结构体的大小 这步操作完成后redisObject 结构体中的指针 ptr 的指向位置就如下图所示它会指向SDS结构头的末尾同时也是字符数组的起始位置 最后createEmbeddedStringObject 函数会把参数中传入的指针 ptr 指向的字符串拷贝到 SDS 结构体中的字符数组并在数组最后添加结束字符这部分代码如下所示 下面这张图也展示了 createEmbeddedStringObject 创建嵌入式字符串的过程可以再整体来看看 总之你可以记住Redis 会通过设计实现一块连续的内存空间把 redisObject 结构体和SDS 结构体紧凑地放置在一起这样一来对于不超过 44 字节的字符串来说就可以避免内存碎片和两次内存分配的开销了而除了嵌入式字符串之外Redis 还设计了压缩列表和整数集合这也是两种紧凑型的内存数据结构 压缩列表和整数集合的设计 首先要知道List、Hash 和 Sorted Set 这三种数据类型都可以使用压缩列表ziplist来保存数据压缩列表的函数定义和实现代码分别在 ziplist.h 和 ziplist.c 中不过在 ziplist.h 文件中其实根本看不到压缩列表的结构体定义这是因为压缩列表本身就是一块连续的内存空间它通过使用不同的编码来保存数据这里为了方便理解压缩列表的设计与实现先来看看它的创建函数 ziplistNew如下所示 实际上ziplistNew 函数的逻辑很简单就是创建一块连续的内存空间大小为ZIPLIST_HEADER_SIZE 和 ZIPLIST_END_SIZE 的总和然后再把该连续空间的最后一个字节赋值为 ZIP_END表示列表结束另外要注意的是在上面代码中定义的三个宏 ZIPLIST_HEADER_SIZE、ZIPLIST_END_SIZE 和 ZIP_END在 ziplist.c 中也分别有定义分别表示 ziplist 的列表头大小、列表尾大小和列表尾字节内容如下所示 那么在创建一个新的 ziplist 后该列表的内存布局就如下图所示注意此时列表中还没有实际的数据 然后当往 ziplist 中插入数据时ziplist 就会根据数据是字符串还是整数以及它们的大小进行不同的编码这种根据数据大小进行相应编码的设计思想正是 Redis 为了节省内存而采用的那么ziplist 是如何进行编码呢要学习编码的实现要先了解 ziplist 中列表项的结构ziplist 列表项包括三部分内容分别是前一项的长度prevlen、当前项长度信息的编码结果encoding以及当前项的实际数据data下面的图展示了列表项的结构图中除列表项之外的内容分别是 ziplist 内存空间的起始和尾部 实际上所谓的编码技术就是指用不同数量的字节来表示保存的信息在 ziplist 中编码技术主要应用在列表项中的 prevlen 和 encoding 这两个元数据上而当前项的实际数据data则正常用整数或是字符串来表示所以这里就先来看下 prevlen 的编码设计ziplist 中会包含多个列表项每个列表项都是紧挨着彼此存放的如下图所示 而为了方便查找每个列表项中都会记录前一项的长度因为每个列表项的长度不一样所以如果使用相同的字节大小来记录 prevlen就会造成内存空间浪费举个例子假设统一使用 4 字节记录prevlen如果前一个列表项只是一个字符串“redis”长度为 5 个字节那么用 1 个字节8 bits就能表示 256 字节长度2 的8 次方等于 256的字符串了此时prevlen 用 4 字节记录其中就有 3 字节是浪费掉了再回过头来看ziplist 在对 prevlen 编码时会先调用 zipStorePrevEntryLength函数用于判断前一个列表项是否小于 254 字节如果是的话那么 prevlen 就使用 1 字节表示否则zipStorePrevEntryLength 函数就调用 zipStorePrevEntryLengthLarge 函数进一步编码这部分代码如下所示 也就是说zipStorePrevEntryLengthLarge 函数会先将 prevlen 的第 1 字节设置为 254然后使用内存拷贝函数 memcpy将前一个列表项的长度值拷贝至 prevlen 的第 2 至第 5 字节最后zipStorePrevEntryLengthLarge 函数返回 prevlen 的大小为 5 字节 在了解了 prevlen 使用 1 字节和 5 字节两种编码方式后再来学习下 encoding 的编码方法一个列表项的实际数据既可以是整数也可以是字符串整数可以是 16、32、64等字节长度同时字符串的长度也可以大小不一所以ziplist 在 zipStoreEntryEncoding 函数中针对整数和字符串就分别使用了不同字节长度的编码结果下面的代码展示了 zipStoreEntryEncoding 函数的部分代码可以看到当数据是不同长度字符串或是整数时编码结果的长度 len 大小不同 简而言之针对不同长度的数据使用不同大小的元数据信息prevlen 和 encoding这种方法可以有效地节省内存开销当然除了 ziplist 之外Redis 还设计了一个内存友好的数据结构这就是整数集合intset它是作为底层结构来实现 Set 数据类型的和 SDS 嵌入式字符串、ziplist 类似整数集合也是一块连续的内存空间这一点从整数集合的定义中就可以看到intset.h 和 intset.c 分别包括了整数集合的定义和实现下面的代码展示了 intset 的结构定义可以看到整数集合结构体中记录数据的部分就是一个 int8_t 类型的整数数组 contents从内存使用的角度来看整数数组就是一块连续内存空间所以这样就避免了内存碎片并提升了内存使用效率 到这里就已经了解了 Redis 针对内存开销所做的数据结构优化分别是 SDS 嵌入式字符串、压缩列表和整数集合而除了对数据结构做优化Redis 在数据访问上也会尽量节省内存开销 节省内存的数据访问 在 Redis 实例运行时有些数据是会被经常访问的比如常见的整数Redis 协议中常见的回复信息包括操作成功“OK”字符串、操作失败ERR以及常见的报错信息所以为了避免在内存中反复创建这些经常被访问的数据Redis 就采用了共享对象的设计思想这个设计思想很简单就是把这些常用数据创建为共享对象当上层应用需要访问它们时直接读取就行现在就来做个假设有 1000 个客户端都要保存“3”这个整数如果 Redis 为每个客户端都创建了一个值为 3 的 redisObject那么内存中就会有大量的冗余而使用了共享对象方法后Redis 在内存中只用保存一个 3 的 redisObject 就行这样就有效节省了内存空间以下代码展示的是 server.c 文件中创建共享对象的函数 createSharedObjects可以看下