第二章 数据结构:快速的Redis有哪些慢操作?

第二章 数据结构:快速的Redis有哪些慢操作?

redis 表现突出的两个原因

  • 在内存中进行操作
  • 高效的数据结构

Redis 常见数据类型

redis 键值对中 值 的数据类型,也就是数据的保存形式

String、List、Hash、Set、Sorted Set、Bitmap、GeoHash、HyperLogLogs、Streams

Redis 常见的几种数据结构说一下?各自的使用场景?

  • string
    • 介绍:string 数据结构是简单的 key-value 类型。
    • 使用场景: 一般常用在需要计数的场景,比如用户的访问次数、热点文章的点赞转发数量等等。
  • list
    • 介绍:list 即是 双向链表
    • 使用场景:发布与订阅或者说消息队列、慢查询。
  • hash
    • 介绍:hash 类似于 JDK1.8 前的 HashMap,内部实现也差不多(数组 + 链表)。
    • 使用场景:系统中对象数据的存储。
  • set
    • 介绍:set 类似于 Java 中的 HashSet。Redis 中的 set 类型是一种无序集合,集合中的元
      素没有先后顺序。当你需要存储一个列表数据,又不希望出现重复数据时,set 是一个很好的选择,并且 set 提供了判断某个成员是否在一个 set 集合内的重要接口,这个也是 list 所不能提供的。可以基于 set 轻易实现交集、并集、差集的操作。
    • 使用场景: 需要存放的数据不能重复以及需要获取多个数据源交集和并集等场景。
  • sorted set
    • 介绍:和 set 相比,sorted set 增加了一个权重参数 score,使得集合中的元素能够按
      score 进行有序排列,还可以通过 score 的范围来获取元素的列表。有点像是 Java 中
      HashMap 和 TreeSet 的结合体。
    • 使用场景:需要对数据根据某个权重进行排序的场景。比如在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息。
  • bitmap
  • 介绍:bitmap 存储的是连续的二进制数字(0 和 1),通过 bitmap, 只需要一个 bit 位来表示某个元素对应的值或者状态,key 就是对应元素本身 。我们知道 8 个 bit 可以组成一个byte,所以 bitmap 本身会极大的节省储存空间。
  • 使用场景:适合需要保存状态信息(比如是否签到、是否登录…)并需要进一步对这些信息进行分析的场景。比如用户签到情况、活跃用户情况、用户行为统计(比如是否点赞过某个视频)。

Redis 底层数据结构

redis 底层数据结构一共有 6 种

简单动态字符串、双向链表、压缩列表、哈希表、跳表、整数数组

  • 简单动态字符串 O(1)
  • 双向链表 O(n)
  • 压缩列表 O(n)
  • 哈希表 O(1)
  • 跳表 O(logN)
  • 整数数组 O(n)
  • String 类型的底层实现只有一种数据结构,也就是简单动态字符串。
  • 而 List、Hash、Set 和 Sorted Set 这四种数据类型,都有两种底层实现结构。通常情况下,我们会把这四种类型称为集合类型,它们的特点是一个键对应了一个集合的数据。
  • String:通过全局 hash 表查到值就能直接操作
  • 集合类型:有两种底层实现结构,哈希表和跳表实现“快”,整数数组和压缩列表节省内存空间。

这些数据结构都是值的底层实现,键和值本身之间用什么结构组织 ?

  • 为了实现从键到值的快速访问,Redis 使用一个哈希表来保存所有键值对(全局哈希表
  • 一个哈希表,其实就是一个数组,数组的每个元素称为一个哈希桶。
  • 一个哈希表是由多个哈希桶组成的,每个哈希桶中保存了键值对数据。
  • 哈希桶中的元素保存的并不是值本身,而是指向具体值的指针。这也就是说,不管值是 String,还是集合类型,哈希桶中的元素都是指向它们的指针。

哈希桶中的 entry 元素中保存了 key value 指针,分别指向了实际的键和值,这样一来,即使值是一个集合,也可以通过 *value 指针被查找到。

第二章 数据结构:快速的Redis有哪些慢操作?_第1张图片

  • 哈希表的最大特点是可以用 O(1) 的时间复杂度来快速查找到键值对。
  • 我们只需要计算键的哈希值,就可以知道它所对应的哈希桶位置,然后就可以访问到相应的 entry 元素。

Redis

为什么哈希表操作变慢了 ?

  • 当多个 key 进行哈希计算时,有可能不同的key计算出了相同的哈希值,这就产生了哈希冲突。
  • Redis 解决哈希冲突的方式,就是链式哈希。同一个哈希桶中的多个元素用一个链表来保存,它们之间依次用指针连接。

举例:哈希冲突

  • entry1、entry2 和 entry3 都需要保存在哈希桶 3 中,导致了哈希冲突。
  • 此时,entry1 元素会通过一个 next 指针指向 entry2,同样,entry2 也会通过next指针指向 entry3。
  • 这样一来,即使哈希桶 3 中的元素有 100 个,我们也可以通过 entry 元素中的指针,把它们连起来。这就形成了一个链表,也叫作哈希冲突链。

第二章 数据结构:快速的Redis有哪些慢操作?_第2张图片

  • 如果哈希冲突越来越多,形成的冲突链表就会过长,导致在这个链上的元素查找耗时较长,效率降低。

针对于上面存在的问题,redis 引入了 rehash 操作

  • rehash 也就是增加现有的哈希桶数量,让逐渐增多的 entry 元素能在更多的桶之间分散保存,减少单个桶中的元素数量,从而减少单个桶中的冲突。
  • 为了使 rehash 操作更高效,Redis 默认使用了两个全局哈希表:哈希表 1 和哈希表 2。
  • 一开始,当你刚插入数据时,默认使用哈希表 1,此时的哈希表 2 并没有被分配空间。随着数据逐步增多,Redis 开始执行 rehash,这个过程分为三步:
    • 给哈希表 2 分配更大的空间,例如是当前哈希表 1 大小的两倍;
    • 把哈希表 1 中的数据重新映射并拷贝到哈希表 2 中;
    • 释放哈希表 1 的空间。
  • 到此,我们就可以从哈希表 1 切换到哈希表 2,用增大的哈希表 2 保存更多数据,而原来的哈希表 1 留作下一次 rehash 扩容备用。

把哈希表 1 中的数据重新映射并拷贝到哈希表 2 中 这一步又会存在问题,如果一次性把哈希表 1 中的数据都迁移完,会造成 Redis 线程阻塞,无法服务其他请求。此时,Redis 就无法快速访问数据了。

  • 为了避免这个问题,Redis 采用了渐进式 rehash
  • 在第二步拷贝数据时,Redis 仍然正常处理客户端请求,每处理一个请求时,从哈希表 1 中的第一个索引位置开始,顺带着将这个索引位置上的所有 entries 拷贝到哈希表 2 中;
  • 等处理下一个请求时,再顺带拷贝哈希表 1 中的下一个索引位置的 entries。
  • 如下图所示:

第二章 数据结构:快速的Redis有哪些慢操作?_第3张图片

  • 渐进式 rehash 技术使用了离散的概念,将迁移数据的工作量均摊到每次操作中,避免迁移造成不可用。代价就是在较长的时间内存在两个哈希表。

问题:一次请求一个 entrys,那后续如果再也没有请求来的时候,余下的entrys是怎么处理的呢 ? 是就留在 hash1中了还是有定时任务后台更新过去呢?

  • 渐进式 rehash 执行时,除了根据键值对的操作来进行数据迁移,Redis 本身还会有一个定时任务在执行 rehash;
  • 如果没有键值对操作时,这个定时任务会周期性地(例如每100ms一次)搬移一些数据到新的哈希表中,这样可以缩短整个 rehash 的过程。

什么是压缩列表(ziplist) ?

压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结构。一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值。具体组成如下

Untitled

属性介绍

第二章 数据结构:快速的Redis有哪些慢操作?_第4张图片

  • zlbytes:表示压缩列表占用的内存(单位:字节)
  • zltail:压缩列表起始指针到尾节点的偏移量
    • 如果我们有一个指向压缩列表起始地址的指针p,通过 p+zltail 就能直接访问压缩列表的最后一个节点
  • zllen:压缩列表中的节点数
  • entry:压缩列表中的节点

压缩列表中的节点

每个压缩列表节点都由 previous_entry_length、encoding、content 三个部分组成,如下图:

Untitled

typedef struct zlentry {
    unsigned int prevrawlensize; 
    unsigned int prevrawlen;     
    unsigned int lensize;        
    unsigned int len;           
    unsigned int headersize;     
    unsigned char encoding;      
    unsigned char *p;          
} zlentry;

previous_entry_length

节点的 previous_entry_length 属性以字节为单位,记录了压缩列表中前一个节点的长度,其值长度为1个字节或者5个字节

  • 如果前一节点的长度小于254字节,那么 previous_entry_length 属性的长度为1字节
    • 前一节点的长度就保存在这一个字节里面
  • 如果前一节点的长度大于等于254字节,那么 previous_entry_length 属性的长度为5字节
    • 其中属性的第一字节会被设置为 0xFE(十进制值254),而之后的四个字节则用于保存前一节点的长度
  • 通过 previous_entry_length 属性,可以方便地访问当前节点的前一个节点

encoding

节点的 encoding 属性记录了节点的 content 属性所保存数据的类型以及长度

content

节点的 content 属性负责保存节点的值,节点值可以是一个字节数组或者整数,值的类型和长度由节点的encoding属性决定。

什么是跳表(skiplist) ?

  • 有序链表只能逐一查找元素,导致操作起来非常缓慢,于是就出现了跳表。具体来说,跳表是在链表的基础上,增加了多级索引,通过索引位置的几个跳转,实现数据的快速定位,如下图所示:

第二章 数据结构:快速的Redis有哪些慢操作?_第5张图片

  • 为了提高查找速度,我们来增加一级索引:从第一个元素开始,每两个元素选一个出来作为索引。这些索引再通过指针指向原始的链表。
  • 例如,从前两个元素中抽取元素 1 作为一级索引,从第三、四个元素中抽取元素 11 作为一级索引。此时,我们只需要 4 次查找就能定位到元素 33 了。
  • 如果我们还想再快,可以再增加二级索引:从一级索引中,再抽取部分元素作为二级索引。例如,从一级索引中抽取 1、27、100 作为二级索引,二级索引指向一级索引。这样,我们只需要 3 次查找,就能定位到元素 33 了。
  • 可以看到,这个查找过程就是在多级索引上跳来跳去,最后定位到元素。这也正好符合“跳”表的叫法。当数据量很大时,跳表的查找复杂度就是 O(logN)

Redis 底层数据结构的时间复杂度

第二章 数据结构:快速的Redis有哪些慢操作?_第6张图片

四字口诀:

  • 单元素操作是基础;
    • 指每一种集合类型对单个数据实现的增删改查操作。
  • 范围操作非常耗时;
    • 指集合类型中的遍历操作,可以返回集合中的所有数据。
  • 统计操作通常高效;
    • 指集合类型对集合中所有元素个数的记录。
  • 例外情况只有几个。

你可能感兴趣的:(Redis高级,redis,压缩列表,跳跃表,数据结构,哈希表)