Redis 核心数据结构理论解析

一、简述

redis是一个开源的使用C语言编写的一个kv存储系统,是一个速度非常快的非关系远程内存数据库。它支持包括String、List、Set、Zset、hash五种数据结构。

除此之外,通过复制、持久化和客户端分片等特性,用户可以很方便地将redis扩展成一个能够包含数百GB数据和每秒处理上百万次的请求的系统。目前支持多种语言的api,方便用户使用。

redis同时也内置了事务、LUA脚本、复制等功能,提供两种持久化选项,一种是每隔一段时间将数据导入到磁盘(RDB快照模式),另一种是追加命令到日志中(AOF模式)。如果只是作为高效的内存数据库使用也可以关闭持久化功能。

通过哨兵(sentinel)和自动分区(Cuuster)的方式可以提高redis服务器的高可用性。

与关系型数据库相比,redis的命令请求不需要经过查询分析器或查询优化器进行处理,也避免了更新数据时引起的随机读\写,这些慢操作。它直接读写内存中的数据,并且数据是按照一定的数据结构存储的,所以它的速度非常快。

  1. redis命令手册:http://www.redis.cn/commands.html
  2. redis 命令说明:https://www.redis.net.cn/order/

二、数据类型

声明:这里的数据类型是value的数据类型,key的数据类型(区分大小写)都是字符串;

1.1 数据类型的使用场景分别是什么?

Redis 提供了丰富的数据类型,常见的有五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)、Zset(有序集合)。

随着 Redis 版本的更新,后面又支持了四种数据类型: BitMap(2.2 版新增)、HyperLogLog(2.8 版新增)、GEO(3.2 版新增)、Stream(5.0 版新增)。 Redis 五种数据类型的应用场景:

  1. String 类型的应用场景:缓存对象、常规计数、分布式锁、共享 session 信息等。
  2. List 类型的应用场景:消息队列(但是有两个问题:1. 生产者需要自行实现全局唯一 ID;2. 不能以消费组形式消费数据)等。
  3. Hash 类型:缓存对象、购物车等。
  4. Set 类型:聚合计算(并集、交集、差集)场景,比如点赞、共同关注、抽奖活动等。
  5. Zset 类型:排序场景,比如排行榜、电话和姓名排序等。

Redis 后续版本又支持四种数据类型,它们的应用场景如下:

  1. BitMap(2.2 版新增):二值状态统计的场景,比如签到、判断用户登陆状态、连续签到用户总数等;
  2. HyperLogLog(2.8 版新增):海量数据基数统计的场景,比如百万级网页 UV 计数等;
  3. GEO(3.2 版新增):存储地理位置信息的场景,比如滴滴叫车;
  4. Stream(5.0 版新增):消息队列,相比于基于 List 类型实现的消息队列,有这两个特有的特性:自动生成全局唯一消息ID,支持以消费组形式消费数据。

1.2 五种常见的 Redis 数据类型是怎么实现?

Redis 数据类型和底层数据结构的对应关图,上边是 Redis 6.0 之前版本,现在看还是有点过时了,下边是现在 Redis 7.0 版本的。
Redis 核心数据结构理论解析_第1张图片

1.3 String 类型的内部实现

String 类型的底层的数据结构实现主要是 SDS(简单动态字符串)。 SDS 和我们认识的 C 字符串不太一样,之所以没有使用 C 语言的字符串表示,因为 SDS 相比于 C 的原生字符串:

  1. SDS 不仅可以保存文本数据,还可以保存二进制数据。因为 SDS 使用 len 属性的值而不是空字符来判断字符串是否结束,并且 SDS 的所有 API 都会以处理二进制的方式来处理 SDS 存放在 buf[] 数组里的数据。所以 SDS 不光能存放文本数据,而且能保存图片、音频、视频、压缩文件这样的二进制数据。
  2. SDS 获取字符串长度的时间复杂度是 O(1)。因为 C 语言的字符串并不记录自身长度,所以获取长度的复杂度为 O(n);而 SDS 结构里用 len 属性记录了字符串长度,所以复杂度为 O(1)。
  3. Redis 的 SDS API 是安全的,拼接字符串不会造成缓冲区溢出。因为 SDS 在拼接字符串之前会检查 SDS 空间是否满足要求,如果空间不够会自动扩容,所以不会导致缓冲区溢出的问题。

1.4 为什么重新设计 SDS 数据结构?

C语言没有Java里面的String类型,只能是靠自己的char[]来实现,字符串在 C 语言中的存储方式,想要获取 「Redis」的长度,需要从头开始遍历,直到遇到 ‘\0’ 为止。所以,Redis 没有直接使用 C 语言传统的字符串标识,而是自己构建了一种名为简单动态字符串 SDS(simple dynamic string)的抽象类型,并将 SDS 作为 Redis 的默认字符串。

C语言的char[]数组 和SDS字符串的对比
Redis 核心数据结构理论解析_第2张图片
C 源码中体现形式
Redis 核心数据结构理论解析_第3张图片

1.5 SDS 底层物理编码方式有哪些?

SDS底层物理编码由 int、embstr、raw 三种方式组成。其中embstr 与 raw 类型底层的数据结构其实都是 SDS (简单动态字符串,Redis 内部定义 sdshdr 一种结构)。只有整数才会使用 int,如果是浮点数, Redis 内部其实先将浮点数转化为字符串值,然后再保存。

不同编码类型的对比
在这里插入图片描述
底层数据结构体现
Redis 核心数据结构理论解析_第4张图片

1.5 List 类型内部实现

Redis3.0 之前

在Redis3.0之前,list采用的底层数据结构是ziplist压缩列表+linkedList双向链表,然后在高版本的Redis中底层数据结构是quicklist(替换了ziplist+linkedList),而quicklist也用到了ziplist。

  1. 如果列表的元素个数小于 512 个(默认值,可由 list-max-ziplist-entries 配置),列表每个元素的值都小于 64 字节(默认值,可由 list-max-ziplist-value 配置),Redis 会使用压缩列表作为 List 类型的底层数据结构;
  2. 如果列表的元素不满足上面的条件,Redis 会使用双向链表作为 List 类型的底层数据结构;

优缺点分析

Redis 核心数据结构理论解析_第5张图片

Redis3.0 之后

主要通过quicklist实现,它实际上是 zipList 和 linkedList 的混合体,它将 linkedList按段切分,每一段使用 zipList 来紧凑存储,多个 zipList 之间使用双向指针串接起来。
Redis 核心数据结构理论解析_第6张图片
quicklist就是「双向链表 + 压缩列表」组合,因为一个 quicklist 就是一个链表,而链表中的每个元素又是一个压缩列表

Redis7 实现

因为ziplist存在连续更新问题,所以在redis7 废除 ziplist 底层结构, 使用新的数据结构 listpack 紧凑链表,彻底解决这个问题。
List 使用 quicklist 来存储,quicklist 存储了双向链表,每个节点都是一个 listpack。

redis7 源码体现
Redis 核心数据结构理论解析_第7张图片

1.6 已有 ziplist,为什么又出 listpack?

listpack 是 Redis 设计用来取代掉 ziplist 的数据结构,它通过每个节点记录自己的长度且放在节点的尾部,来彻底解决掉了 ziplist 存在的连锁更新的问题。

ziplist 的连锁更新问题

1)ziplist存储结构
Redis 核心数据结构理论解析_第8张图片Redis 核心数据结构理论解析_第9张图片
2)复现场景

压缩列表新增某个元素或修改某个元素时,如果空间不不够,压缩列表占用的内存空间就需要重新分配。而当新插入的元素较大时,可能会导致后续元素的 prevlen 占用空间都发生变化,从而引起「连锁更新」问题,导致每个元素的空间都要重新分配,造成访问压缩列表性能的下降。

案例说明:压缩列表每个节点正因为需要保存前一个节点的长度字段,就会有连锁更新的隐患

第一步:现在假设一个压缩列表中有多个连续的、长度在 250~253 之间的节点,如下图:
因为这些节点长度值小于 254 字节,所以 prevlen 属性需要用 1 字节的空间来保存这个长度值,一切OK,O(∩_∩)O哈哈~
在这里插入图片描述

第二步:这时,如果将一个长度大于等于 254 字节的新节点加入到压缩列表的表头节点,即新节点将成为entry1的前置节点,如下图:
Redis 核心数据结构理论解析_第10张图片
因为entry1节点的prevlen属性只有1个字节大小,无法保存新节点的长度,此时就需要对压缩列表的空间重分配操作并将entry1节点的prevlen 属性从原来的 1 字节大小扩展为 5 字节大小。

第三步:连续更新问题出现
Redis 核心数据结构理论解析_第11张图片
entry1节点原本的长度在250~253之间,因为刚才的扩展空间,此时entry1节点的长度就大于等于254,因此原本entry2节点保存entry1节点的 prevlen属性也必须从1字节扩展至5字节大小。entry1节点影响entry2节点,entry2节点影响entry3节点…一直持续到结尾。

这种在特殊情况下产生的连续多次空间扩展操作就叫做「连锁更新」

1.7 Hash 类型内部实现

Hash 类型的底层数据结构是由压缩列表或哈希表实现的:

  1. 如果哈希类型元素个数小于 512 个(默认值,可由 hash-max-ziplist-entries 配置),所有值小于 64 字节(默认值,可由 hash-max-ziplist-value 配置)的话,Redis 会使用压缩列表作为 Hash 类型的底层数据结构;
  2. 如果哈希类型元素不满足上面条件,Redis 会使用哈希表作为 Hash 类型的底层数据结构。
    在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。

1.8 Set 类型内部实现

Redis用整数集合(intset)或 哈希表 hashtable存储set。

  1. 如果集合中的元素都是整数且元素个数小于 512 (默认值,set-maxintset-entries配置)个,Redis 会使用整数集合(intset)作为 Set 类型的底层数据结构;
  2. 如果集合中的元素不满足上面条件,则 Redis 使用哈希表(数组+链表)作为 Set 类型的底层数据结构,key就是元素的值,value为null

1.9 ZSet 类型内部实现

Zset 类型的底层数据结构是由压缩列表或跳表实现的:

  1. 如果有序集合的元素个数小于 128 个,并且每个元素的值小于 64 字节时,Redis 会使用压缩列表作为 Zset 类型的底层数据结构;
  2. 如果有序集合的元素不满足上面的条件,Redis 会使用跳表作为 Zset 类型的底层数据结构;

在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。

三、总结

本章介绍了redis的十大数据结构和它们使用的底层存储原理,为了达到节省内存和快速访问的目的每种数据结构可能有两种存储和访问结构,在必要的时候会由一种结构转换成另一种结构,但这个转换的过程会消耗系统性能和内存空间的,所以在使用的过程中需要注意这些配置参数,开发中尽量避免达到这些峰值,使得redis能够持续的提供高效的服务。

3.1 类型以及常见场景

  1. String 类型的应用场景:缓存对象、常规计数、分布式锁、共享 session 信息等。
  2. List 类型的应用场景:消息队列(但是有两个问题:1. 生产者需要自行实现全局唯一 ID;2. 不能消费组形式消费数据)等。
  3. Hash 类型:缓存对象、购物车等。
  4. Set 类型:聚合计算(并集、交集、差集)场景,比如点赞、共同关注、抽奖活动等。
  5. Zset 类型:排序场景,比如排行榜、电话和姓名排序等。

Redis 后续版本又支持四种数据类型,它们的应用场景如下:

  1. BitMap(2.2 版新增):二值状态统计的场景,比如签到、判断用户登陆状态、连续签到用户总数等;
  2. HyperLogLog(2.8 版新增):海量数据基数统计的场景,比如百万级网页 UV 计数等;
  3. GEO(3.2 版新增):存储地理位置信息的场景,比如滴滴叫车;
  4. Stream(5.0 版新增):消息队列,相比于基于 List 类型实现的消息队列,有这两个特有的特性:自动生成全局唯一消息ID,支持以消费组形式消费数据。

3.2 底层数据类型对应底层数据结构

1)String (字符串)

1. int:8个字节的长整型。
2. embstr:小于等于44个字节的字符串。
3. raw:大于44个字节的字符串。

Redis会根据当前值的类型和长度决定使用哪种内部编码实现。

2)Hash(哈希)

ziplist(压缩列表):当哈希类型元素个数小于hash-max-ziplist-entries 配置(默认512个)、同时所有值都小于hash-max-ziplist-value配置(默认64 字节)时,Redis会使用ziplist作为哈希的内部实现,ziplist使用更加紧凑的 结构实现多个元素的连续存储,所以在节省内存方面比hashtable更加优秀。

hashtable(哈希表):当哈希类型无法满足ziplist的条件时,Redis会使 用hashtable作为哈希的内部实现,因为此时ziplist的读写效率会下降,而hashtable的读写时间复杂度为O(1)。

3)List(列表)

ziplist(压缩列表):当列表的元素个数小于list-max-ziplist-entries配置 (默认512个),同时列表中每个元素的值都小于list-max-ziplist-value配置时 (默认64字节),Redis会选用ziplist来作为列表的内部实现来减少内存的使 用。

linkedlist(链表):当列表类型无法满足ziplist的条件时,Redis会使用 linkedlist作为列表的内部实现。quicklist ziplist和linkedlist的结合以ziplist为节点的链表(linkedlist)

Redis7 开始废弃ziplist(压缩列表)、使用listpack(紧凑链表) 代替。

listpack(紧凑列表):当列表的元素个数小于list-max-listpack-entries配置 (默认512个),同时列表中每个元素的值都小于list-max-listpack-value配置时 (默认64字节),Redis会选用ziplist来作为列表的内部实现来减少内存的使 用。

4)set (集合)

intset(整数集合):当集合中的元素都是整数且元素个数小于set-max-intset-entries配置(默认512个)时,Redis会用intset来作为集合的内部实现,从而减少内存的使用。

hashtable(哈希表):当集合类型无法满足intset的条件时,Redis会使用hashtable作为集合的内部实现。

5)Sorted Set (有序集合)

ziplist(压缩列表):当有序集合的元素个数小于zset-max-ziplist- entries配置(默认128个),同时每个元素的值都小于zset-max-ziplist-value配 置(默认64字节)时,Redis会用ziplist来作为有序集合的内部实现,ziplist 可以有效减少内存的使用。

skiplist(跳跃表):当ziplist条件不满足时,有序集合会使用skiplist作 为内部实现,因为此时ziplist的读写效率会下降。

Redis7 开始废弃ziplist(压缩列表)、使用listpack(紧凑链表) 代替。

listpack(紧凑列表):当列表的元素个数小于list-max-listpack-entries配置 (默认512个),同时列表中每个元素的值都小于list-max-listpack-value配置时 (默认64字节),Redis会选用ziplist来作为列表的内部实现来减少内存的使 用。

3.3 底层数据结构时间复杂度

Redis 核心数据结构理论解析_第12张图片

3.4 数据类型与物理编码对应表

Redis 核心数据结构理论解析_第13张图片

你可能感兴趣的:(#,Redis,redis,数据库,缓存)