02 | 数据结构学习笔记:快速的Redis有哪些慢操作?

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: redis接收到一个键值对操作后,能以微秒级别的速度找到数据,并快速完成操作。

一、redis为什么快?

redis接收到一个键值对操作后,能以微秒级别的速度找到数据,并快速完成操作。


1、所有操作都在内存上完成,内存的访问速度本身就很快

2、归功于它的数据结构,键值对是按一定的数据结构来组织的,操作键值对最终就是对数据结构进行增删改查操作。


  • String(字符串)
  • List(列表)
  • Hash(哈希)
  • Set(集合)
  • Sorted Set(有序集合)


这些是数据的保存形式的类型也就是值的类型,并不是底层的数据结构,这里需要注意。


底层数据结构一共有 6 种,分别是

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

20200808215301958.png

List、Hash、Set 和 Sorted Set 这四种数据类型,把这四种类型称为集合类型,它们的特点是一个键对应了一个集合的数据

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

为了实现从键到值的快速访问,Redis 使用了一个哈希表来保存所有键值对。


一个哈希表,其实就是一个数组,数组的每个元素称为一个哈希桶。


一个哈希表是由多个哈希桶组成的,每个哈希桶中保存了键值对数据


对于集合类型,hash桶是如何存储的呢?


哈希桶中的元素保存的并不是值本身,而是指向具体值的指针。不管值是 String,还是集合类型,哈希桶中的元素都是指向它们的指针。

20200808210040671.png

哈希桶中的 entry 元素中保存了*key和*value指针,分别指向了实际的键和值


如果是一个集合,通过*value指针查找。


哈希表保存了所有的键值对,所以,我也把它称为全局哈希表。

这个查找过程主要依赖于哈希计算,和数据量的多少并没有直接关系。


不管哈希表里有 10 万个键还是 100 万个键,我们只需要一次计算就能找到相应的键。而hashtable的 O(1) 复杂度和快速查找特性。

为什么hash表操作变慢了?

如果redis中写入了大量数据,hashtable变得很大时,哈希冲突问题(当你往哈希表中写入更多数据时,哈希冲突是不可避免的问题。这里的哈希冲突,也就是指,两个 key 的哈希值和哈希桶计算对应关系时,正好落在了同一个哈希桶中。)和 rehash 可能带来的操作阻塞。redis会变慢,这也是官方推荐单节点不能过大的原因。


哈希冲突是不可避免的,因为哈希桶的个数通常要远远少于 key 的数量,难免会有一些 key 的哈希值对应到了同一个哈希桶中。

redis如何解决hash冲突问题?

链式哈希。同一个哈希桶中的多个元素用一个链表来保存,它们之间依次用指针连接。

20200808211556952.png

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

什么时候会发生rehash?

哈希冲突链上的元素只能通过指针逐一查找再操作。如果哈希表里写入的数据越来越多,哈希冲突可能也会越来越多,这就会导致某些哈希冲突链过长,进而导致这个链上的元素查找耗时长,效率降低。


对于追求“快”的 Redis 来说,这是不太能接受的。所以,Redis 会对哈希表做 rehash 操作。rehash 也就是增加现有的哈希桶数量,让逐渐增多的 entry 元素能在更多的桶之间分散保存,减少单个桶中的元素数量,从而减少单个桶中的冲突。


那具体怎么做呢?

为了使 rehash 操作更高效,Redis 默认使用了两个全局哈希表:

哈希表 1 和哈希表 2。


一开始,当你刚插入数据时,默认使用哈希表 1,此时的哈希表 2 并没有被分配空间。随着数据逐步增多,Redis 开始执行 rehash,这个过程分为三步:

  • 给哈希表 2 分配更大的空间,例如是当前哈希表 1 大小的两倍;
  • 把哈希表 1 中的数据重新映射并拷贝到哈希表 2 中;
  • 释放哈希表 1 的空间。


到此,我们就可以从哈希表 1 切换到哈希表 2,用增大的哈希表 2 保存更多数据,而原来的哈希表 1 留作下一次 rehash 扩容备用。这个过程看似简单,但是第二步涉及大量的数据拷贝,如果一次性把哈希表 1 中的数据都迁移完,会造成 Redis 线程阻塞,无法服务其他请求。此时,Redis 就无法快速访问数据了。为了避免这个问题,Redis 采用了渐进式 rehash。

20200808212551953.png

在第二步拷贝数据时,Redis 仍然正常处理客户端请求,每处理一个请求时,从哈希表 1 中的第一个索引位置开始,顺带着将这个索引位置上的所有 entries 拷贝到哈希表 2 中; 等处理下一个请求时,再顺带拷贝哈希表 1 中的下一个索引位置的 entries。


巧妙地把一次性大量拷贝的开销,分摊到了多次处理请求的过程中,避免了耗时操作,保证了数据的快速访问。


String 类型来说,找到哈希桶就能直接增删改查了,所以,哈希表的 O(1) 操作复杂度也就是String 类型的复杂度了。

集合数据操作的复杂度

一个集合类型的值,

  • 第一步是通过全局哈希表找到对应的哈希桶位置
  • 第二步是在集合中再增删改查。

集合的操作效率和哪些因素相关呢?

1、集合的底层数据结构有关,使用哈希表实现的集合,要比使用链表实现的集合访问效率更高;

2、操作效率和这些操作本身的执行特点有关,比如读写一个元素的操作要比读写所有元素的效率高。

整数数组和双向链表

都是顺序读写,通过数组下标或者链表的指针逐个元素访问,操作复杂度基本是 O(N),操作效率比较低;对应的redis 值类型是set 和list。

压缩列表

压缩列表实际上类似于一个数组,数组中的每一个元素都对应保存一个数据。和数组不同的是,压缩列表在表头有三个字段 zlbytes、zltail 和 zllen,分别表示列表长度、列表尾的偏移量和列表中的 entry 个数;压缩列表在表尾还有一个 zlend,表示列表结束。

20200808213847989.png

在压缩列表中,如果我们要查找定位第一个元素和最后一个元素,可以通过表头三个字段的长度直接定位,复杂度是 O(1)。而查找其他元素时,就没有这么高效了,只能逐个查找,此时的复杂度就是 O(N) 了。


这也可以理解POP 和PUSH 在操作list 时很快。

跳表

有序链表只能逐一查找元素,导致操作起来非常缓慢,于是就出现了跳表。


跳表在链表的基础上,增加了多级索引,通过索引位置的几个跳转,实现数据的快速定位。

20200808214227314.png

如果我们要在链表中查找 33 这个元素,只能从头开始遍历链表,查找 6 次,直到找到 33 为止。此时,复杂度是 O(N),查找效率很低。


为了提高查找速度,我们来增加一级索引:从第一个元素开始,每两个元素选一个出来作为索引。这些索引再通过指针指向原始的链表。例如,从前两个元素中抽取元素 1 作为一级索引,从第三、四个元素中抽取元素 11 作为一级索引。此时,我们只需要 4 次查找就能定位到元素 33 了。


如果我们还想再快,可以再增加二级索引:从一级索引中,再抽取部分元素作为二级索引。例如,从一级索引中抽取 1、27、100 作为二级索引,二级索引指向一级索引。这样,我们只需要 3 次查找,就能定位到元素 33 了。


可以看到,这个查找过程就是在多级索引上跳来跳去,最后定位到元素。这也正好符合“跳”表的叫法。当数据量很大时,跳表的查找复杂度就是 O(logN)。 需要注意的跳表的前提是有序链表。

20200808214406286.png

redis操作的复杂度

  • 单元素操作是基础;
  • 范围操作非常耗时;
  • 统计操作通常高效;
  • 例外情况只有几个。


第一,单元素操作,是指每一种集合类型对单个数据实现的增删改查操作。


HGET、HSET 和 HDEL 是对哈希表做操作,所以它们的复杂度都是 O(1);


Set 类型用哈希表作为底层数据结构时,它的 SADD、SREM、SRANDMEMBER 复杂度也是 O(1)。


这里,有个地方你需要注意一下,集合类型支持同时对多个元素进行增删改查,例如 Hash 类型的 HMGET 和 HMSET,Set 类型的 SADD 也支持同时增加多个元素。此时,这些操作的复杂度,就是由单个元素操作复杂度和元素个数决定的。例如,HMSET 增加 M 个元素时,复杂度就从 O(1) 变成 O(M) 了。


第二,范围操作,是指集合类型中的遍历操作,可以返回集合中的所有数据,比如 Hash 类型的 HGETALL 和 Set 类型的 SMEMBERS,或者返回一个范围内的部分数据,比如 List 类型的 LRANGE 和 ZSet 类型的 ZRANGE。这类操作的复杂度一般是 O(N),比较耗时,我们应该尽量避免。


第三,统计操作,是指集合类型对集合中所有元素个数的记录,例如 LLEN 和 SCARD。这类操作复杂度只有 O(1),这是因为当集合类型采用压缩列表、双向链表、整数数组这些数据结构时,这些结构中专门记录了元素的个数统计,因此可以高效地完成相关操作。


第四,例外情况,是指某些数据结构的特殊记录,例如压缩列表和双向链表都会记录表头和表尾的偏移量。这样一来,对于 List 类型的 LPOP、RPOP、LPUSH、RPUSH 这四个操作来说,它们是在列表的头尾增删元素,这就可以通过偏移量直接定位,所以它们的复杂度也只有 O(1),可以实现快速操作。


问题:


整数数组和压缩列表在查找时间复杂度方面并没有很大的优势,那为什么 Redis 还会把它们作为底层数据结构呢?


Redis作者想通过不同编码实现效率和空间的平衡。比如当我们的存储只有10个元素的列表,当使用双向链表数据结构时,必然需要维护大量的内部字段如每个元素需要:前置指针,后置指针,数据指针等,造成空间浪费,如果采用连续内存结构的压缩列表(ziplist),将会节省大量内存,而由于数据长度较小,存取操作时间复杂度即使为O(n2)性能也可满足需求。20200808221600303.png

三、为什么集合类型有那么多的底层结构,它们都是怎么组织数据的,都很快吗?

四、什么是简单动态字符串,和常用的字符串是一回事吗

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
13天前
|
消息中间件 存储 缓存
redis支持的数据结构
redis支持的数据结构
22 2
|
1月前
|
存储 JSON NoSQL
redis基本数据结构(String,Hash,Set,List,SortedSet)【学习笔记】
这篇文章是关于Redis基本数据结构的学习笔记,包括了String、Hash、Set、List和SortedSet的介绍和常用命令。文章解释了每种数据结构的特点和使用场景,并通过命令示例演示了如何在Redis中操作这些数据结构。此外,还提供了一些练习示例,帮助读者更好地理解和应用这些数据结构。
redis基本数据结构(String,Hash,Set,List,SortedSet)【学习笔记】
|
17天前
|
NoSQL Redis C++
Redis的实现五:二叉堆的数据结构和TTL、c,c++的实现
这篇文章详细探讨了二叉堆的数据结构及其在C和C++中的实现,特别强调了二叉堆在Redis中实现TTL(生存时间)功能的重要性,并通过代码示例展示了如何在Redis中使用二叉堆来管理键的过期时间。
28 0
|
2月前
|
存储 监控 NoSQL
redis数据结构-HyperLogLog
redis数据结构-HyperLogLog
41 1
|
2月前
|
存储 NoSQL Redis
redis数据结构-ziplist
redis数据结构-ziplist
23 2
|
2月前
|
存储 NoSQL 数据处理
redis数据结构-Bitmaps
redis数据结构-Bitmaps
33 0
|
2月前
|
存储 缓存 NoSQL
redis数据结构-hash
redis数据结构-hash
21 0
|
存储 NoSQL 算法
「Redis」数据结构与对象
Redis数据结构与对象介绍
|
NoSQL 算法 Java
Redis进阶 - 数据结构:对象机制详解,一文深入底层分析
我们在前文已经阐述了Redis 5种基础数据类型详解,分别是字符串(string)、列表(list)、哈希(hash)、集合(set)、有序集合(zset),以及5.0版本中Redis Stream结构详解;那么这些基础类型的底层是如何实现的呢?Redis的每种对象其实都由对象结构(redisObject) 与 对应编码的数据结构组合而成, 本文主要介绍对象结构(redisObject) 部分。
Redis进阶 - 数据结构:对象机制详解,一文深入底层分析
|
17天前
|
存储 缓存 NoSQL
数据的存储--Redis缓存存储(一)
数据的存储--Redis缓存存储(一)
53 1