2020-10-09Redis 数据结构的底层实现

Redis 数据结构的底层实现

      Redis的性能瓶颈 在于CPU资源, 在于内存访问和 络IO。 采 单线程的设计带来的好处是,极 简化 数据结构和算法的实现。相反,Redis通过异步IO 和pipelining等机制来实现 速的并发访问。显然,单线程的设计,对于单个请求的快速响应时 间也提出 的要求。

一 Dict 

1. 结构

   dict 也是基于key 和value的 结构,redis 本身的数据存储就是通过dict来实现的。

   dict是为了解决查找问题(Searching), 通常查找问题的解法分为两种方式 : 一个是基于各种平衡树, 另一个是基于哈希表。我们平常使 的各种Map或dictionary, 都是基于哈 希表实现的。在 要求数据有序存储,且能保持较低的哈希值冲突概率的前提下,基于哈希表的 找性能能做到 常 效,接近O(1)。

     Redis的dict实现最显著的 个特点,就在于它的重哈 希。它采 种称为增 式重哈希(incremental rehashing)的 法,在需要扩展内存时避免 次性对所有key进 重哈希, 是将重哈希操作分散到对于dict的各个增删改查的操作中去。这种 法能做到每次只对 部分key进 重哈希, 每次重哈希之间 影响dict的操作。dict之所以 样设计,是为 避免重哈希期间单个请求的响应时间剧 增加,这与前 提到的“快速响应时间” 设计原则是相符的。

Redis源码dict.h

dict

     2. rehash

dictRehash每次将重哈希至少向前推进n步(除 到n步整个重哈希就结束 ),每 步都将 ht[0]上某 个bucket(即 个dictEntry链表)上的每 个dictEntry移动到ht[1]上,它在ht[1]上的 位置根据ht[1]的sizemask进行重新计算。rehashidx记录 当前尚未迁移(有待迁移)的ht[0]的 bucket位置。如果dictRehash被调用的时候,rehashidx指向的bucket 个dictEntry也没有,那么它就没有可新迁移的数据。这时它尝试在ht[0].table数组中不断向后遍历,直到找到下一个存有数据的bucket位 置。如果 直找 到,则最多 n*10步,本次重哈希暂告结束。

最后,如果ht[0]上的数据都迁移到ht[1]上 (即d->ht[0].used == 0),那么整个重哈希结束, ht[0]变成ht[1]的内容, ht[1]重置为空。

3. 插入操作

dict的插入 (dictAdd和dictReplace) dictAdd插入新的 对key和value,如果key已经存在,则插入失败。 dictReplace也是插一对key和value, 过在key存在的时候,它会更新value。

  • 它也会触发推进 步重哈希(_dictRehashStep)。

  • 如果正在重哈希中,它会把数据插 到ht[1];否则插 到ht[0]。

  • 在对应的bucket中插 数据的时候,总是插 到dictEntry的头部。因为新数据接下来被访问的 概率可能 较 ,这样再次查找它时就 较次数较少。

  • _dictKeyIndex在dict中寻找插 位置。如果 在重哈希过程中,它只查找ht[0];否则查找ht[0] ht[1]。

  • _dictKeyIndex可能触发dict内存扩展

二. sds

它的全称是Simple Dynamic String

与其它语 环境中出现的字符串相比,它具有如下显 著的特点:

可动态扩展内存。sds表示的字符 其内容可以修改,也可以追加。在很多语言中字符 会分为 mutable和immutable两种,显然sds属于mutable类型的。
进制安全(Binary Safe)。sds能存储任意 进制数据, 仅仅是可打印字符。 与传统的C语 字符 类型兼容。这个的含义接下来 上会讨论。

看到这 ,很多对Redis有所 解的同学可能已经产 个疑问:Redis已经对外 个字符 结构,叫做string,那这 所说的sds到底和string是 么关系呢?可能有 会猜:string是基于sds实 现的。这个猜想已经 常接近事实,但在描述上还 太准确。有关string和sds之间关系的详细分析, 我们放在后 再讲。现在为 讨论,让我们先暂时简单地认为,string的底层实现就是sds。

在讨论sds的具体实现之前,我们先站在Redis使 者的 度,来观察 下string所 持的 些主要操作。操作示 :

(/assets/photos_redis/redis_string_op_examples.png)

以上这些操作都 较简单,我们简单解释 下:

我们知道,在C语 中,字符 是以’\0’字符结尾(NULL结束符)的字符数组来存储的,通常表达为 字符指针的形式(char *)。它 允许字节0出现在字符 中间,因此,它 能 来存储任意的 进制 数据。

sds

上图是sds的 个内部结构的 。图中展示 两个sds字符 s1和s2的内存结构, 个使 sdshdr8 类型的header,另 个使 sdshdr16类型的header。但它们都表达 同样的 一个长度为6的字符的值”leilei“ : 我们结合代码,来解释每 部分的组成。

sds的字符指针(s1和s2)就是指向真正的数据(字符数组)开始的位置, header位于内存地址较 低的 向。在sds.h中有 些跟解析header有关的宏定义:

三 .ziplist

1. 概述     

 ziplist是 个经过特殊编码的双向链表,它的设计 标就是为 提 存储效率。 ziplist可以 于存储字符 或整数,其中整数是按真正的 进制表示进 编码的, 是编码成字符 序 。它能以O(1)的时间复杂度在表的两端提供 push 和 pop 操作.

      一个普通的双向链表,链表中每 项都占 独 的 块内存,各项之间 地址指针(或引 )连接起来。这种 式会带来 的内存碎 , 且地 址指针也会占 额外的内存。 ziplist却是将表中每 项存放在前后连续的地址空间内, 个ziplist整 体占 块内存。它是 个表(list),但其实 是 个链表(linked list)。

1.1. 结构组成

从宏观上看,ziplist的内存结构如下: <zlbytes><zltail><zllen><entry>...<entry><zlend>

<zlbytes> : 32bit,表示ziplist占 的字节总数(也包括 <zlbytes> 本身占 的4个字节)。

<zltail> : 32bit,表示ziplist表中最后 项(entry)在ziplist中的偏移字节数。 <zltail> 的 存在,使得我们可以很 地找到最后 项( 遍历整   个ziplist),从 可以在ziplist尾端快 速地执 push或pop操作。

<zllen> : 16bit, 表示ziplist中数据项(entry)的个数。zllen字段因为只有16bit,所以可以表 达的最 值为2^16-1。这 需要特别注意的是,如果ziplist中数据项个数超过 16bit能表达的 最 值,ziplist仍然可以来表示。那怎么表示呢?这 做 这样的规定:如果 <zllen> 于等 于2^16-2(也就是 等于2^16-1),那么 <zllen> 就表示ziplist中数据项的个数;否则,也就 是 <zllen> 等于16bit全为1的情况,那么 <zllen> 就 表示数据项个数 ,这时候要想知道 ziplist中数据项总数,那么必须对ziplist从头到尾遍历各个数据项,才能计数出来。

<entry> : 表示真正存放数据的数据项, 度 定。 个数据项(entry)也有它 的内部结 构,这个稍后再解释。

<zlend> : ziplist最后1个字节,是 个结束标记,值固定等于255

2.结构例子解析 

2.1

这个ziplist 共包含33个字节。字节编号从byte[0]到byte[32]。图中每个字节的值使 16进制表 示。
2.2.

头4个字节(0x21000000)是按 端(little endian)模式存储的 <zlbytes> 字段。 么是 端 呢?就是指数据的低字节保存在内存的低地址中(参 维基百科词条Endianness (https://en.wikipedia.org/wiki/Endianness))。因此,这 <zlbytes> 的值应该解析成 0x00000021, 进制表示正好就是33。

2.3.

接下来4个字节(byte[4..7])是 <zltail> , 端存储模式来解释,它的值是 0x0000001D(值为29),表示最后 个数据项在byte[29]的位置(那个数据项为0x05FE14)。

2.4.

再接下来2个字节(byte[8..9]),值为0x0004,表示这个ziplist 共存有4项数据。

2.5.

接下来6个字节(byte[10..15])是第1个数据项。其中,prevrawlen=0,因为它前 没有数据 项;len=4,相当于前 定义的9种情况中的第1种,表示后 4个字节按字符 存储数据,数据 的值为”name”。

2.6.

接下来8个字节(byte[16..23])是第2个数据项,与前 数据项存储格式类似,存储1个字符 ”tielei”。

2.7.

接下来5个字节(byte[24..28])是第3个数据项,与前 数据项存储格式类似,存储1个字符 ”age”。

2.8.

接下来3个字节(byte[29..31])是最后 个数据项,它的格式与前 的数据项存储格式 太 样。其中,第1个字节prevrawlen=5,表示前 个数据项占 5个字节;第2个字节=FE,相当于 前 定义的9种情况中的第8种,所以后 还有1个字节 来表示真正的数据,并且以整数表示。 它的值是20(0x14)。

2.9.

最后1个字节(byte[32])表示 <zlend> ,是固定的值255(0xFF)。 总结 下,这个ziplist 存 4个数据项,分别为:

字符 : “name” 字符 : “tielei” 字符 : “age” 整数: 20

总结 下,这个ziplist 存 4个数据项,分别为:

字符 : “name”

字符 : “tielei”

字符 : “age”

整数: 20

3. hash与ziplist

hash是Redis中可以 来存储 个对象结构的 较 想的数据类型。 个对象的各个属性,正好对应

个hash结构的各个field。

我们在 上很容 找到这样 些技术 ,它们会说存储 个对象,使 hash string要节省内存。 实际上这么说是有前提的,具体取决于对象怎么来存储。如果你把对象的多个属性存储到多个key上 (各个属性值存成string),当然占的内存要多。但如果你采 些序 化 法, 如Protocol Buffers (https://github.com/google/protobuf),或者Apache Thrift (https://thrift.apache.org/),先把对 象序 化为字节数组,然后再存 到Redis的string中,那么跟hash相 ,哪 种 省内存,就 定 。

当然,hash 序 化后再存 string的 式,在 持的操作命令上,还是有优势的:它既 持多个field 同时存取( hmset / hmget ),也 持按照某个特定的field单独存取( hset / hget )。

实际上,hash随着数据的增 ,其底层数据结构的实现是会发 变化的,当然存储效率也就 同。在 field 较少,各个value值也 较 的时候,hash采 ziplist来实现; 随着field增多和value值增 , hash可能会变成dict来实现。当hash底层变成dict来实现的时候,它的存储效率就没法跟那些序 化 式相 。

当我们为某个key第 次执 hset key field value 命令的时候,Redis会创建 个hash结构, 这个新创建的hash底层就是 个ziplist。

当随着数据的插 ,hash底层的这个ziplist就可能会转成dict。那么到底插 多少才会转呢?

使用到的Redis配置:

 hash-max-ziplist-entries 512
 hash-max-ziplist-value 64

这个配置的意思是说,在如下两个条件之 满 的时候,ziplist会转成dict:

当hash中的数据项(即field-value对)的数 超过512的时候,也就是ziplist数据项超过1024的 时候(请参考t_hash.c中的 hashTypeSet 函数)。 当hash中插 的任意 个value的 度超过 64的时候(请参考t_hash.c中的

hashTypeTryConversion 函数)。

四. quicklist • skiplist

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值