见微知著 —— Redis 字符串内部结构源码分析

本篇咱们开始讲字典 key 的内部结构,也就是 sds 字符串。首先它不是普通字符串,而是 sds 字符串,这个 sds 的意思是「Simple Dynamic String」,它的结构很简单,它是动态的,意味着能够支持修改。不过即便是这样简单的字符串结构,在结构设计上做者但是煞费苦心。面试

咱们知道 C语言里面的字符串是以0x\0结尾,一般就说是以 NULL 结尾。它不包含长度信息,当咱们须要获取字符串长度时,须要调用 strlen(s) 来获取长度,它的时间复杂度是 O(n),若是一个字符串太长,这个函数就太浪费 CPU了。redis

因此 Redis 不能这么干,它须要将长度信息使用单独的字段进行存储,这就须要一个额外的字段,这个字段也要占用存储空间。在平常使用中,小字符串才是大头,它的长度信息每每只须要 1byte 存储就能够了,能够表示最大长度为 255 的字符串。若是字符串再大一些,就须要 2byte,甚至是 3byte、4byte。Redis 会为不一样长度的字符串选择不一样长度的字段来表示长度信息。同时 Redis 为了能够直接使用标准C语言字符串库函数,sds 的字符串内容仍是以 NULL 结尾,这会额外多占用一个字节的空间。缓存

sds 是动态字符串,它须要支持追加操做,须要能扩充容量。若是字符串放置的比较紧凑,追加时,就须要从新分配新的更大的存储空间,而后进行内容的拷贝(不严格,想一想为何)。若是追加的太频繁,内存的分配和拷贝就会消耗大量 CPU。架构

图片函数

因此 Redis 为动态字符串设计了冗余空间,追加时只要内容不是太大,是能够没必要从新分配内存的,若是字符串的长度是1024,Redis 会分配2048字节的存储空间,也就是 100% 的冗余空间。这个设计很是相似于 Java 语言的 ArrayList 。不过 Redis 考虑的更加周到,当字符串的长度超过 1M 时,它的冗余空间只有 1M,避免出现太大的浪费。Redis 还限制了字符串最大长度不得超过 512M。性能

下面是 sds 字符串的结构定义源码学习

 

咱们平常使用的字符串都是只读的,通常只有拿字符串当位图使用时才会对字符串进行追加和修改操做。为了不浪费,Redis 在第一次建立 sds 字符串时,不给它分配冗余空间。在第一次追加操做以后才会分配 100% 的冗余空间。优化

图片编码

值得注意的是,咱们平时使用的字符串指针都是指向字符串内存空间的头部,可是在 Redis 里面咱们使用的 sds 字符串指针指向的是字符串内存空间的脖子部位,由于 sds 字符串有本身的头部信息。debug

若是 sds 字符串只是做为字典的 key 而存在,那么字典里面元素的 key 会直接指向 sds。若是 字符串是做为 Redis的对象而存在,它还会包上一个通用的对象头,也就是 RedisObject。对象头的 ptr 字段会指向 sds。

 

讲到这里,须要提一下现代计算机的结构上在 CPU 和 内存之间存在一个缓存的结构,用来协调 CPU 的高效和访存的相对缓慢的矛盾。咱们平时听到的 L1 Cache、L2 Cache就是这个缓存。当 CPU 要访问内存时先在缓存里找一找有没有,若是没有就去内存里拿了以后放到缓存里,这个缓存的最小单位通常是 64 字节,也就是一次性缓存连续的 64 字节内容,这个最小单位称为「缓存行」。这样下次获取内存地址附近的数据时能够直接从缓存中拿到。

对于 Redis 的字符串对象来讲,咱们须要先访问 redisObject 对象头,拿到 ptr 指针,而后再访问指向的 sds 字符串。若是对象头和 sds 字符串相距较远,就会存在缓存穿透现象,性能就会打折。因此 Redis 为了优化硬件的缓存命中,它为字符串设计了一种特殊的编码结构,这种结构就是 embstr 。它将 redisObject 对象头和 sds 字符串挤在一块儿连续存储,能够一次性放到缓存行里,这样就能够明显提高缓存命中率。

 

 

object 指令观察一下对象的编码类型来验证一下这个计算是否正确。

 

注意到上面的输出中出现了 encoding:int 类型的编码,这是怎么回事呢?原来 Redis 又对整型字符串作了优化,当字符串是能够用 long 类型表达的整数时,Redis 内部将会使用整型编码。注意整数在 Redis 内部的类型 type 是字符串。

 

咱们再观察一遍 redisObject 对象头。

 

当字符串内容能够用 long 整数表达时,对象头的 ptr 指针将退化为一个 long 型的整数。也就是

 

若是这个整数太大,超出了 long 的表达范围,就会使用 sds 字符串表示,根据长短不一样会分别选择 embstr 和 raw 编码类型。

咱们再看一个很诡异的现象

 

注意 debug object 指令输出的 Value at: xxxxxxx 这个表示 redisObject 对象头的地址。为何值为 9999 时,两个对象的地址是同样的。而变成了 10000 地址就不同了呢?

 

这是由于「小整数对象缓存」。Redis 在初始化的时候会构造 [0, 10000) 这1w个小整数对象持久放在内存里,之后凡是在这个范围内的整型字符串都会直接使用共享的小整数对象。小整数对象的引用计数字段的值恒定为 INT_MAX。在不少面向对象的语言中,都有小整数对象缓存的概念。

接下来咱们仔细分析一下建立 embstr 的函数 createEmbeddedStringObject 的代码

 

咱们能够看到对象头和字符串内容是经过一次zmalloc调用分配的,也就是说对象头和字符串内容是连续的分配在一块儿。还将 sds 字符串的 flags 设置为 SDS_TYPE_8 说明它是一个短字符串,长度能够直接用一个字节就能够表示。同时在字符串内容 buf 的尾部有 '\0' 标识,这是 C 字符串的结束标志。

欢迎工做一到五年的Java工程师朋友们加入Java架构开发:744677563

本群提供免费的学习指导 架构资料 以及免费的解答

不懂得问题均可以在本群提出来 以后还会有职业生涯规划以及面试指导

相关文章
相关标签/搜索