redis数据结构分析(下)

在上一篇,redis数据结构分析(上)中已经分析了动态字符串和链表。

这一篇中主要分析字典,跳表和整数集合这三种数据结构。

字典

字典又称为符号表(symbol table)、关联数组(associative array)或映射(map),采用key-value的存储方式。

在实现结构上采用二维结构,第一维是数组,第二维是链表, key和value存放在链表中,数组里存放的是链表的头指针。

添加新键值对时,先用字典设置的哈希函数根据key算出哈希值,再用哈希值和哈希表的 sizemask 属性值按位与运算得出包含此键值对的哈希节点在哈希数组中的索引值,找到索引值对应的链表头,再将键值对保存在链表中。链表是用来解决hash冲突的。

  

字典的结构

1587480253831)(/Users/zhangmanyi/Downloads/redis-hash.png)]

使用示例

在字典中增加元素,可以使用hset一次增加一个键值对,也可以使用hmset一次增加多个键值对。

> hset ireader go fast
(integer) 1
> hmset ireader java fast python slow
OK
> hget ireader go
"fast"
> hset ireader go fast11
(integer) 0
> hget ireader go
"fast11"

字典中的每个key都是独一无二的,程序可以在字典中根据键查找与之关联的值。如果添加相同key的键值对,后面的会覆盖前面的。

在非rehash的时候,只会使用ht[0]。ht[1]只用于rehash时,对字典进行扩容和收缩处理的中间保存。

  

rehash

rehash扩展与收缩条件
  • 目前没有正在执行bgsave或者bgrewriteaof命令,则负载因子大于等于1时开始扩容.
  • 目前正在执行bgsave或者bgrewriteaof命令,负载因子大于等于5时开始扩容.
  • 负载因子小于0.1时,程序自动开始收缩

因为在BGSAVE或BGREWRITEAOF命令时,Redis会创建子进程,应避免在子进程存在期间进行扩展操作,来节约内存。

计算负载因子方式: load_factor = ht[0].used / ht[0].size;

举例:

对于一个大小为4的,包含4个键值对的哈希表来说,这个哈希表的负载因子为:lf = 4/4 = 1;

注意:size=4,used=4并不表示已经存储满了,也有可能全部在t[0]节点上形成了一个4个节点的链表哦.所以used=20, size=4也是有可能的.

扩容

在新增元素时,如果达到了扩容的条件,则自动进行扩容。

  • 为字典ht[1]分配空间,空间大小为ht[0].used*2的最近最大的一个2的n次方(2的n次方幂).
  • 将ht[0]中的所有键值对移动到ht1[1]上面,rehash指的是重新计算键的哈希值和索引值.
  • 当ht[0]中的键值对都移动到ht1[0]时,将ht[1]设置为ht[0],并在ht[1]新创建一个空白的哈希表.
缩容

在删除元素时,如果达到了缩容的条件,则自动进行缩容。

  • 为字典ht[1]分配空间,空间大小为ht[0].used 最近最大的一个的2的n次方.
  • 将ht[0]中的所有键值对移动到ht1[1]上面,rehash指的是重新计算键的哈希值和索引值.
  • 当ht[0]中的键值对都移动到ht1[0]时,将ht[1]设置为ht[0],并在ht[1]新创建一个空白的哈希表.
计算ht[1]分配空间举例

扩容时,ht[0]的used=3, 那么3*2=6,找到6最近的最大的2的幂次方值.那就是8,则扩展空间ht[1]的分配空间就是8。

缩容时,ht[0]的used=9,那么9*2=18,找到18最近最大的2的幂次方值,那就是32了,则扩展空间ht[1]的分配空间就是32。

渐进式rehash

如果hash结构很大,比如有上百万个键值对,那么一次完整rehash的过程就会耗时很长。这对于单线程的Redis里来说有点压力山大。

所以Redis采用了渐进式rehash的方案。它会同时保留两个新旧hash结构,在后续的定时任务以及hash结构的读写指令中将旧结构的元素逐渐迁移到新的结构中。这样就可以避免因扩容导致的线程卡顿现象。

img

参考:https://blog.csdn.net/men_wen/article/details/69787532

执行过程
  1. 字典结构dict中的一个成员rehashidx,记录当前rehash的进度。当rehashidx为-1时表示不进行rehash,当rehashidx值为0时,表示开始进行rehash。
  2. 在rehash期间,字典的删除,查找,更新等操作会先搜索ht[0],并且判断是否在进行rehash,如果是则顺带进行单步rehash,rehashidx每移动一个元素+1;如果在ht[0]中找不到指定元素再去ht[1]中找。如果是新增,则直接加在ht[1]中。
  3. 当rehash进行完成时,将rehashidx置为-1,表示完成rehash。

  

跳表

参考:https://redisbook.readthedocs.io/en/latest/internal-datastruct/skiplist.html

对链表稍加改造,就可以支持类似“二分”的查找算法。我们把改造之后的数据结构叫作跳表(Skip list)。

跳跃表在 Redis 的两个地方有用到, 一个是实现有序集合键(zset),还有一个是在集群节点中用作内部数据结构。

当有序集合中包含的元素数量超过服务器属性 server.zset_max_ziplist_entries 的值(默认值为 128 ),或者有序集合中新添加元素的 member 的长度大于服务器属性 server.zset_max_ziplist_value 的值(默认值为 64 )时,redis会使用跳跃表作为有序集合的底层实现。

否则会使用ziplist作为有序集合的底层实现。

  

使用示例

通过zadd指令可以增加一到多个value/score对,score放在前面。

通过zrank指令获取指定元素的正向排名,这里的排名是通过跨度计算得到的。

> zadd ireader 4.0 python
(integer) 1
> zadd ireader 4.0 java 1.0 go
(integer) 2
> zrank ireader java
(integer) 1   

  

一般跳表的结构

在链表之上加多级索引。每加一层索引之后,查找一个结点需要遍历的结点个数就减少了,也就是说查找效率提高了。

img

  

redis中跳表的结构

Redis 的跳跃表 主要由两部分组成:zskiplist(链表)和zskiplistNode (节点)

../_images/skiplist.png

在这里插入图片描述
图片最左边是一个zskiplist的结构,包含以下属性:
在这里插入图片描述
图片的右边是四个zskiplistNode结构,包含以下属性:
在这里插入图片描述在这里插入图片描述

每个节点都带有一个高度为 1 层的后退指针,用于从表尾方向向表头方向迭代:当执行 ZREVRANGEZREVRANGEBYSCORE 这类以逆序处理有序集的命令时,就会用到这个属性。

zskiplist只是用于持有这些zskiplistNode节点,便于程序的管理和快速访问。

zskiplistNode节点按照score从小到大来排序。

每个zskiplistNode节点中保存的成员对象必须唯一,但是多个节点的score可以相同,score相同的节点会按照成员对象的字典序来决定节点的位置。较小的节点会排在更靠近表头的位置。

zskiplistNode的层高

在这里插入图片描述
zskiplistNode结点具有哪几层是由层高决定的,而层高是在创建结点时,随机生成的一个介于1到32之间的值,上图从左到右展示了层高为1、3、5的zskiplistNode结点。

  

跳表查询元素的过程

img
查找score为2.0,成员对象为o2的节点:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cAfeaCrC-1587480106224)(D:\Users\zhangm005\AppData\Roaming\Typora\typora-user-images\1587447524586.png)]
1、首先访问跳跃表的第一个节点(表头),然后从第四层的前进指针移动到表中的第二个节点。

2、在第二个节点时,从第二层的前进指针制动到表的第三个节点,找到score为2.0,成员对象为o2的节点。

程序经过了两个跨度为1的节点,因此目标节点在跳跃表中的排位为2。

  

跳表 vs 红黑树

1 内存占用方面跳表比红黑树多,但是多的内存很有限
2 实现比红黑树简单
3 跟红黑树更方便的支持范围查询

  

整数集合

整数集合(intset)是集合键的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素不多时,Redis就会使用整数集合做为集合键的底层实现。

> sadd key1 1 3 5 7 9
> object encoding key1
"intset"

  

整数集合的结构

typede struct intset{
//编码方式
uint32_t encoding;

//集合包含的元素数量
uint32_t length;
//元素数组
int8_t contents[];
}intset;

contents数组是整个集合的底层实现,整数集合的每个元素都是contents数组的一个数组项,每个项在数组中按值的大小从小到大有序地排列,并且数组中不包含任何重复项。

img

encoding=intset_enc_int16,表示集合保存的都是int16类型的整数值,如果是intset_enc_int64,则表示集合保存的都是int64类型的整数值.

数组大小的计算:sizeof(int16_t)*length=16*4 ,如果是int64,就是64*length.

  

Redis中整数集合与C语言中的数组区别

  • C语言是静态类型语言,为了避免错误,通常不会将两种类型的值存放到一起.比如int16_t类型的数组只会用来保存int16类型的值,而32位类型的值只会用int32_t类型的数组来保存。

  • Redis中整个集合中可以任意的存储16,32,64位的数据,可以通过自动升级来完成类型转换,而不需要用户关心内存溢出的问题。

  

整数集合自动升级

> sadd key1 1 3 5 7 9

这里可以看出1,3,5,7,9只需要使用int16_t类型的数组来存储就OK了,key1所占空间大小为16*5=80。

但是当在key1中再添加一个值1232323432423423235235后,这个时候key1=[1,3,5,7,9,1232323432423423235235]。

之前的key1使用int16_t的数组就能够存储了,但是现在新增加的元素是64位的,之前的数组明显不能够存储64位的值,所以就会导致key1原先的数组升级,key1需要升级到int64_t的数组才能够存储。

redis也就是在存储时发现不够存储了,开始升级操作。

redis> sadd key1 1232323432423423235235

升级后的key1空间大小为: 64*6=384。

升级步骤
  • “1”元素之前存在了0-15位, “3”元素存储到了16-31位,依次类推。
  • 现在告诉我们16位变成64位了。
  • “9”元素排名第5位,“9”开始移动,从原先的(64-79)移动到(256-319),以此类推。
  • “1”元素最后占用了(0-63)的位置,“3”元素占到了(64-127)的位置。

升级移动是从右到左的,就是先从排序最大的开始移动.
在这里插入图片描述

为什么要升级?

如果没有升级操作,那么刚开始定空间大小如何定呢?直接上来就用64位?如果我只存储1,3,5,7,9用64位空间就太浪费了。所以redis为了节约空间,根据数据大小来定数组空间大小。
  

降级

整数集合不支持降级操作,一旦对数组进行了升级,编码会一直保持升级后的状态。

  

参考

(字典)https://juejin.im/post/5b53ee7e5188251aaa2d2e16

(字典)http://www.baowenwei.com/post/fen-bu-shi/redisshe-ji-yuan-li-zhi-di-ceng-shu-ju-jie-gou#toc_9

(字典)https://blog.csdn.net/men_wen/article/details/69787532

(字典)https://www.jianshu.com/p/bfecf4ccf28b

(跳表)https://redisbook.readthedocs.io/en/latest/internal-datastruct/skiplist.html

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值