Redis数据结构和数据类型

        Redis能高效的处理,除了在内存中进行操作,还有一个原因就是它的数据结构和类型。

数据结构

RedisObject

        Redis中的任意数据类型的键和值都会被封装为一个RedisObject,也叫做Redis对象。

从Redis的使用者的角度来看,⼀个Redis节点包含多个database(非cluster模式下默认是16个,cluster模式下只能是1个),而一个database维护了key space 到 value space的映射关系。其中key是string类型,value可以是任意类型:如string、List、Set、Zset、Map。

⽽从Redis内部实现的⾓度来看,database内的这个映射关系是用⼀个dict来维护的。dict的key固定用⼀种数据结构来表达就够了,这就是动态字符串sds。而value则比较复杂,为了在同⼀个dict内能够存储不同类型的value,这就需要⼀个通⽤的数据结构,这个通用的数据结构就是robj,全名是redisObject。

SDS

        Redis中保存的Key是字符串,通常情况下Value也是字符串或者字符串集合,因此字符串使用的很频繁。Redis是c实现的,为了提高效率,并没有采用c的字符串结构,c的字符串所存在的问题:

  • 获取字符串长度需要运算
  • 不可修改
  • 非二进制安全(c的字符串以'\0'结尾)

为了优化以上问题,Redis提出了SDS,之所以叫做SDS(动态字符串)是因为它具备动态扩容的功能。

如上图SDS的数据结构,可以知道:

  • 通过len获取字符串长度的时间复杂度变成O(1)
  • 不需要用 “\0” 字符来标识字符串结尾了,而是有个专门的 len 成员变量来记录长度,所以可存储包含 “\0” 的数据。
  • 通过alloc - len 可以在进行修改操作的时候判断是否扩容

IntSet

        IntSet是Redis中set集合的一种实现方式,基于整数数组来实现,并且具备长度可变、有序等特征。

Intset可以看做是特殊的整数数组,具备一些特点:

  • Redis会确保Intset中的元素唯一、有序

  • 具备类型升级机制,可以节省内存空间

  • 底层采用二分查找方式来查询

Dict

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

        Dict由三部分组成:字典,哈希表、哈希节点

        当我们向Dict添加键值对时,Redis首先根据key计算出hash值(h),然后利用 h & sizemask来计算元素应该存储到数组中的哪个索引位置。我们存储k1=v1,假设k1的哈希值h =1,则1&3 =1,因此k1=v1要存储到数组角标1位置。

Dict的扩容

        Dict中的哈希表采用的是数组+链表的方式实现,在数据量过大,链表长度会变长查询效率降低,因此需要进行扩容,每次插入新值都会检查负载因子(used/size),以下两种情况会进行扩容:

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

哈希表的初始化大小为4,扩容大小为第一个大于等于used+1的2^n

Dict的rehash

        从上图可以看到dict中定义了两个哈希表,

在正常服务请求阶段,插入的数据,都会写入到「哈希表 1」,此时的「哈希表 2 」 并没有被分配空间。随着数据逐步增多,触发了 rehash 操作,这个过程分为三步:

  • 给「哈希表 2」 分配空间,一般会比「哈希表 1」 大一倍(两倍的意思);
  • 将「哈希表 1 」的数据迁移到「哈希表 2」 中;
  • 迁移完成后,「哈希表 1 」的空间会被释放,并把「哈希表 2」 设置为「哈希表 1」,然后在「哈希表 2」 新创建一个空白的哈希表,为下次 rehash 做准备。

上面这过程看起来没啥问题,但是第二步在数据量大的时候,迁移会涉及大量的数据拷贝,可能会对 Redis 造成阻塞,无法服务其他请求。因此提出了渐进式hash。

渐进式hash

渐进式hash也就是将一次大迁移分成多个小迁移。

渐进式hash过程:

  • 给「哈希表 2」 分配空间;
  • 在 rehash 进行期间,每次哈希表元素进行新增、删除、查找或者更新操作时,Redis 除了会执行对应的操作之外,还会顺序将「哈希表 1 」中索引位置上的所有 key-value 迁移到「哈希表 2」 上;
  • 随着处理客户端发起的哈希表操作请求数量越多,最终在某个时间点会把「哈希表 1 」的所有 key-value 迁移到「哈希表 2」,从而完成 rehash 操作。

在渐进式hash的过程中,数据是分配在两个哈希表中的,因此操作要在两个表上进行,比如,查找一个 key 的值的话,先会在「哈希表 1」 里面进行查找,如果没找到,就会继续到哈希表 2 里面进行找到。

另外,新增操作只会在表2新增,保证了表1的数据一直减少,知道变成空表rehash结束。

ZipList

        ZipList 是一种特殊的“双端链表” ,由一系列特殊编码的连续内存块组成。可以在任意一端进行压入/弹出操作, 并且该操作的时间复杂度为 O(1)。

        ZipList的Entry不像其他链表节点那样,存储上下节点指针,因为记录两个指针占用16字节,它采用的是下面的结构:

例如,我们要保存字符串:“ab”和 “bc”

  • previous_entry_length:前一节点的长度,占1个或5个字节(条件在下面)。
  • encoding:编码属性,记录content的数据类型(字符串还是整数)以及长度,占用1个、2个或5个字节。

  • contents:负责保存节点的数据,可以是字符串或整数

上面这种结构也还是存在一些问题的,比如连锁更新:

ZipList的每个Entry都包含previous_entry_length来记录上一个节点的大小,长度是1个或5个字节:

  • 如果前一节点的长度小于254字节,则采用1个字节来保存这个长度值
  • 如果前一节点的长度大于等于254字节,则采用5个字节来保存这个长度值,第一个字节为0xfe,后四个字节才是真实长度数据

现在,假设我们有N个连续的、长度为250~253字节之间的entry,因此entry的previous_entry_length属性用1个字节即可表示,如图所示:

这里多添加了一个254字节的数据进来,原来后续节点的previous_entry_length都为1字节,现在都需要变成5字节,发生了连锁更新。

QuickList

介绍QuickList之前可以先看一下ZipList的缺点和解决办法:

        ZipList虽然节省内存,但是需要分配连续的内存空间,在内存占用多的情况下怎么办?

  •         为了解决这个问题,需要限制ZipList的长度和Entry的大小。

        我们要存储大量数据,超出了ZipList最佳的上限该怎么办?

  •         我们可以创建多个ZipList来分片存储数据,提高查询效率。

        数据拆分后比较分散,不方便管理和查找,这多个ZipList如何建立联系?

  •         QuickList就是用来解决这个问题的

QuickList它是一个双端链表,只不过链表中的每个节点都是一个ZipList。

为了避免QuickList中的每个ZipList中entry过多,Redis提供了一个配置项:list-max-ziplist-size来限制。 如果值为正,则代表ZipList的允许的entry个数的最大值 如果值为负,则代表ZipList的最大内存大小,这里就不在一一介绍了。

SkipList

        Redis只有Zset底层实现用到了调表,跳表的优势是能支持平均 O(logN) 复杂度的节点查找。

跳表是在链表的基础上改进过来的,实现了一种多层的有序列表。下面是一个层级为3的调表:

图中头节点有 L0~L2 三个头指针,分别指向了不同层级的节点,然后每个层级的节点都通过指针连接起来:

  • L0 层级共有 5 个节点,分别是节点1、2、3、4、5;
  • L1 层级共有 3 个节点,分别是节点 2、3、5;
  • L2 层级只有 1 个节点,也就是节点 3 。

假如我们需要查找节点4的数据,使用链表需要查找4次,而使用跳表,从L2层直接定位到节点3,只需在向后查找一次。

节点根据score属性来进行排序

数据类型

数据类型是在数据结构基础上进行了封装,redis直接操作的是数据类型。

String

String是Redis中最常见的数据存储类型:

其基本编码方式是RAW,基于简单动态字符串(SDS)实现,存储上限为512mb。

String总共有三种编码方式,如下所示:

List

Redis的List类型可以从首、尾操作列表中的元素:

那有下面三种结构可以满足:

LinkedList:普通链表,可以从双端访问,内存占用较高,内存碎片较多

ZipList:压缩列表,可以从双端访问,内存占用低,存储上限低

QuickList:LinkedList + ZipList,可以从双端访问,内存占用较低,包含多个ZipList,存储上限高

在3.2版本之前,Redis采用ZipList和LinkedList来实现List,当元素数量小于512并且元素大小小于64字节时采用ZipList编码,超过则采用LinkedList编码。

在3.2版本之后,Redis统一采用QuickList来实现List:

Set

Set是Redis中的单列集合,满足以下特点:

  • 不保证有序
  • 保证唯一
  • 可以进行差集、并集、交集

可以看出,Set对查询元素的效率要求非常高,思考一下,什么样的数据结构可以满足? HashTable,也就是Redis中的Dict,不过Dict是双列集合(可以存键、值对)

Set是Redis中的集合,不一定确保元素有序,可以满足元素唯一、查询效率要求极高。 为了查询效率和唯一性,set采用HT编码(Dict)。Dict中的key用来存储元素,value统一为null

ZSet

ZSet也就是SortedSet,其中每一个元素都需要指定一个score值和member值:

  • 可以根据score值排序
  • member必须唯一
  • 可以根据member查询socre

因此,zset底层数据结构必须满足键值存储、键必须唯一、可排序这几个需求。前面的哪种编码结构可以满足?

  • SkipList:可以排序,并且可以同时存储score和ele值(member)

  • HT(Dict):可以键值存储,并且可以根据key找value

当元素数量不多时,HT和SkipList的优势不明显,而且更耗内存。因此zset还会采用ZipList结构来节省内存,不过需要同时满足两个条件:

  • 元素数量小于zset_max_ziplist_entries,默认值128

  • 每个元素都小于zset_max_ziplist_value字节,默认值64

ziplist本身没有排序功能,而且没有键值对的概念,因此需要有zset通过编码实现:

  • ZipList是连续内存,因此score和element是紧挨在一起的两个entry, element在前,score在后

  • score越小越接近队首,score越大越接近队尾,按照score值升序排列

Hash

Hash结构与Redis中的Zset非常类似:

  • 都是键值存储

  • 都需求根据键获取值

  • 键必须唯一

hash结构如下:

zset集合如下:

因此,Hash底层采用的编码与Zset也基本一致,只需要把排序有关的SkipList去掉即可:

Hash结构默认采用ZipList编码,用以节省内存。 ZipList中相邻的两个entry 分别保存field和value

当数据量较大时,Hash结构会转为HT编码,也就是Dict,触发条件有两个:

  • ZipList中的元素数量超过了hash-max-ziplist-entries(默认512)

  • ZipList中的任意entry大小超过了hash-max-ziplist-value(默认64字节)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值