Redis的六种数据结构和五种数据类型

Redis六种数据结构

1. 简单动态字符串(Simple Dynamic String),简称SDS

我们知道Redis当中保存的key为字符串类型,value也往往是字符串或者字符串集合。可见字符串是Redis中最常见的一种数据结构。虽然Redis是用C语言实现的,但是Redis并没有使用C语言中的字符串,因为C语言中的字符串(C语言中声明的字符串,本质是字符数组)存在很多问题:比如

1. 需要通过运算获取字符串长度

2. 非二进制安全(不能包含特殊字符,'\n')

3. 不可修改

Redis是C语言实现的,其中SDS是一个结构体,源码如下:(刚开始时len和alloc值相同,后面不一定相等

uint8_t 表示无符号,整型,占8个bit位,即len的最大值为255,对应数组buf[]最大255个字节,在C语言中,char占一个字节,所以数组最大存放255个字符。

由于SDS是根据字符串长度来获取字符串的,所以不会存在由于遇到特殊字符'\0'而没有读取到完整字符串就停止读取的情况。获取字符串长度只要读取len即可,不需要进行计算。

另外,SDS之所以叫动态字符串,是因为它具备动态扩容的能力。例如一个内容为"hi"的SDS

len:2alloc:2flags:1hi\0

假如我们要给SDS追加一段字符串",Amy",首先会申请新的内存空间

 1.  如果新字符串小于1M,则新空间为扩展后字符串长度的两倍+1;

 2.  如果新字符串长度大于1M,则新空间为扩展后字符串长度+1M+1,称为内存预分配

总结可以得出SDS的-以下几个优点:

   1. 获取字符串长度的时间复杂度为O(1)

   2. 支持动态扩容

   3. 减少内存分配次数

   4. 二进制安全

2. 整数数组 IntSet

IntSet是Redis中Set集合的一种实现方式,基于整数数组实现,具有长度可变和有序等特征。

为了方便查找,Redis将IntSet中的所有整数按照升序依次保存在contents数组中。(头部分固定占8个字节)

IntSet升级

现在,假设有一个intset,元素为{5,10,20},采用的编码是INTSET_ENC_INT16,则每个整数占2字节:

我们向该其中添加一个数字:50000,这个数字超出了int16_t的范围,intset会自动升级编码方式到合适的大小。
以当前案例来说流程如下:

1 升级编码为INTSET_ENC_INT32,每个整数占4字节,并按照新的编码方式及元素个数扩容数组

2 倒序依次将数组中的元素拷贝到扩容后的正确位置

3 将待添加的元素放入数组末尾

4 最后,将inset的encoding属性改为INTSET_ENC_INT32,将length属性改为4

3. Dict

我们知道Redis是一个键值型(Key-Value Pair)的数据库,我们可以根据键实现快速的增删改查。而键与值的映射关系正是通过Dict来实现的。

Dict由三部分组成,分别是:哈希表(DictHashTable)、哈希节点(DictEntry,键和值的数据类型大多数都是指针,指向SDS对象)、字典(Dict)

当我们向Dict添加键值对时,Redis首先根据key计算出hash值(h),然后利用h&sizemask来计算元素应该存储到数组中的哪个索引位置。(size一定是2^{n},h&sizemask和h%size效果一样,但是h&sizemask性能更高)。我们存储k1=v1,假设k1的哈希值h=1,则1&3=1,因此k1=v1要存储到数组角标1位置。存储k2=v2,假设哈希值也是h=1,也要存储到角标1位置,采用头插法进行插入。

Dict的扩容

Dict中的HashTable就是数组结合单向链表的实现,当集合中元素较多时,必然导致哈希冲突增多,链表过长,则查询效率会大大降低。

Dict在每次新增键值对时都会检查负载因子(LoadFactor=used/size),满足以下两种情况时会触发哈希表扩容:

◆ 哈希表的LoadFactor>=1,并且服务器没有执行BGSAVE或者BGREWRITEAOF等后台进程;

◆ 哈希表的LoadFactor>5;

Dict的收缩

Dict除了扩容以外,每次删除元素时,也会对负载因子做检查,当LoadFactor<0.1时,会做哈希表收缩:

Dict的rehash

不管是扩容还是收缩,必定会创建新的哈希表,导致哈希表的size和sizemask变化,而key的查询与sizemask有关。因此必须对哈希表中的每一个key重新计算索引,插入新的哈希表,这个过程称为rehash。过程是这样的:
1. 计算新hash表的realeSize,值取决于当前要做的是扩容还是收缩:

    ◆ 如果是扩容,则新size为第一个大于等于dict.ht[0].used+1的2^{n}

    ◆如果是收缩,则新size为第一个大于等于dict.ht[0].used的2^{n}(不得小于4)

2. 按照新的realeSize申请内存空间,创建dictht,并赋值给dict.ht[1]

3. 设置dict.rehashidx=0,标示开始rehash

4. 将dict.ht[0]中的每一个dictEntry都rehash到dict.ht[1]

Dict的rehash并不是一次性完成的。试想一下,如果Dict中包含数百万的entry,要在一次rehash完成,极有可能导致主线程阻塞。因此Dict的rehash是分多次、渐进式的完成,因此称为渐进式rehash。

4. 每次执行新增、查询、修改、删除操作时,都检查一下dict.rehashidx是否大于-1,如果是则将dict.ht[0].table[rehashidx]的entry链表rehash到dict.ht[1],并且将rehashidx++。直至dict.ht[0]的所有数据都rehash到dict.ht[1] (rehash过程中数据要么在dict.ht[1]中要么在dict.ht[0]中,不会同时在两个表中存在,所以要在两个表中都进行查改删,新增只需在dict.ht[1]中)

5. 将dict.ht[1]赋值给dict.ht[0],给dict.ht[1]初始化为空哈希表,释放原来的dict.ht[0]的内存

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值