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

redis 为什么要这莫快?一个就是他是基于内存的,另外一个就是他是他的数据结构

说到这儿,你肯定会说:“这个我知道,不就是 String(字符串)、List(列表)、
Hash(哈希)、Set(集合)和 Sorted Set(有序集合)吗?”其实,这些只是 Redis 键
值对中值的数据类型,也就是数据的保存形式。而这里,我们说的数据结构,是要去看看
它们的底层实现。

简单来说,底层数据结构一共有 6 种,分别是简单动态字符串、双向链表、压缩列表、哈
希表、跳表和整数数组。它们和数据类型的对应关系如下图所示

 可以看到,String 类型的底层实现只有一种数据结构,也就是简单动态字符串。而 List、
Hash、Set 和 Sorted Set 这四种数据类型,都有两种底层实现结构。通常情况下,我们会
把这四种类型称为集合类型,它们的特点是一个键对应了一个集合的数据

以上 底层的数据结构我单写一篇文章

看到这里,其实有些问题已经值得我们去考虑了:

        这些数据结构都是值的底层实现,键和值本身之间用什么结构组织?
        为什么集合类型有那么多的底层结构,它们都是怎么组织数据的,都很快吗?
        什么是简单动态字符串,和常用的字符串是一回事吗?

接下来,我就和你聊聊前两个问题。这样,你不仅可以知道 Redis“快”的基本原理,还
可以借此理解 Redis 中有哪些潜在的“慢操作”,最大化 Redis 的性能优势。

键和值用什么结构组织?

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

一个哈希表,其实就是一个数组,数组的每个元素称为一个哈希桶。所以,我们常说,一
个哈希表是由多个哈希桶组成的,每个哈希桶中保存了键值对数据。

看到这里,你可能会问了:“如果值是集合类型的话,作为数组元素的哈希桶怎么来保存
呢?”其实,哈希桶中的元素保存的并不是值本身,而是指向具体值的指针。这也就是
说,不管值是 String,还是集合类型,哈希桶中的元素都是指向它们的指针。

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

 因为这个哈希表保存了所有的键值对,所以,我也把它称为全局哈希表。哈希表的最大好
处很明显,就是让我们可以用 O(1) 的时间复杂度来快速查找到键值对——我们只需要计算
键的哈希值,就可以知道它所对应的哈希桶位置,然后就可以访问相应的 entry 元素。

你看,这个查找过程主要依赖于哈希计算,和数据量的多少并没有直接关系。也就是说,
不管哈希表里有 10 万个键还是 100 万个键,我们只需要一次计算就能找到相应的键

但是,如果你只是了解了哈希表的 O(1) 复杂度和快速查找特性,那么,当你往 Redis 中
写入大量数据后,就可能发现操作有时候会突然变慢了。这其实是因为你忽略了一个潜在
的风险点,那就是哈希表的冲突问题和 rehash 可能带来的操作阻塞。

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

当你往哈希表中写入更多数据时,哈希冲突是不可避免的问题。这里的哈希冲突,也就是
指,两个 key 的哈希值和哈希桶计算对应关系时,正好落在了同一个哈希桶中。

毕竟,哈希桶的个数通常要少于 key 的数量,这也就是说,难免会有一些 key 的哈希值对
应到了同一个哈希桶中

Redis 解决哈希冲突的方式,就是链式哈希。链式哈希也很容易理解,就是指同一个哈希
桶中的多个元素用一个链表来保存,它们之间依次用指针连接。

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

 但是,这里依然存在一个问题,哈希冲突链上的元素只能通过指针逐一查找再操作。如果
哈希表里写入的数据越来越多,哈希冲突可能也会越来越多,这就会导致某些哈希冲突链
过长,进而导致这个链上的元素查找耗时长,效率降低。对于追求“快”的 Redis 来说,
这是不太能接受的。

所以,Redis 会对哈希表做 rehash 操作。rehash 也就是增加现有的哈希桶数量,让逐渐
增多的 entry 元素能在更多的桶之间分散保存,减少单个桶中的元素数量,从而减少单个
桶中的冲突。那具体怎么做呢?

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

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

到此,我们就可以从哈希表 1 切换到哈希表 2,用增大的哈希表 2 保存更多数据,而原来
的哈希表 1 留作下一次 rehash 扩容备用.

这个过程看似简单,但是第二步涉及大量的数据拷贝,如果一次性把哈希表 1 中的数据都
迁移完,会造成 Redis 线程阻塞,无法服务其他请求。此时,Redis 就无法快速访问数据
了.

为了避免这个问题,Redis 采用了渐进式 rehash

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

 这样就巧妙地把一次性大量拷贝的开销,分摊到了多次处理请求的过程中,避免了耗时操
作,保证了数据的快速访问。
好了,到这里,你应该就能理解,Redis 的键和值是怎么通过哈希表组织的了。对于
String 类型来说,找到哈希桶就能直接增删改查了,所以,哈希表的 O(1) 操作复杂度也就
是它的复杂度了。
但是,对于集合类型来说,即使找到哈希桶了,还要在集合中再进一步操作。接下来,我
们来看集合类型的操作效率又是怎样的

有哪些底层数据结构?

集合类型的底层数据结构主要有 5 种:整数数组、双向链表、哈
希表、压缩列表和跳表

其中,哈希表的操作特点我们刚刚已经学过了;整数数组和双向链表也很常见,它们的操
作特征都是顺序读写,也就是通过数组下标或者链表的指针逐个元素访问,操作复杂度基
本是 O(N),操作效率比较低;压缩列表和跳表我们平时接触得可能不多,但它们也是
Redis 重要的数据结构,所以我来重点解释一下。


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

 

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

我们再来看下跳表。

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

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

 

不同操作的复杂度

集合类型的操作类型很多,有读写单个集合元素的,例如 HGET、HSET,也有操作多个元
素的,例如 SADD,还有对整个集合进行遍历操作的,例如 SMEMBERS。这么多操作,
它们的复杂度也各不相同。而复杂度的高低又是我们选择集合类型的重要依据。我总结了一个“四句口诀”,希望能帮助你快速记住集合常见操作的复杂度。这样你在使用过程中,就可以提前规避高复杂度操作了

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

第一,单元素操作,是指每一种集合类型对单个数据实现的增删改查操作。例如,Hash 类
型的 HGET、HSET 和 HDEL,Set 类型的 SADD、SREM、SRANDMEMBER 等。这些操
作的复杂度由集合采用的数据结构决定,例如,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),比较耗时,
我们应该尽量避免。
不过,Redis 从 2.8 版本开始提供了 SCAN 系列操作(包括 HSCAN,SSCAN 和
ZSCAN),这类操作实现了渐进式遍历,每次只返回有限数量的数据。这样一来,相比于
HGETALL、SMEMBERS 这类操作来说,就避免了一次性返回所有元素而导致的 Redis 阻
塞。

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

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

edis 之所以能快速操作键值对,一方面是因为 O(1) 复杂度的哈希表被广泛使用,包括
String、Hash 和 Set,它们的操作复杂度基本由哈希表决定,另一方面,Sorted Set 也采
用了 O(logN) 复杂度的跳表。不过,集合类型的范围操作,因为要遍历底层数据结构,复
杂度通常是 O(N)。这里,我的建议是:用其他命令来替代,例如可以用 SCAN 来代替,
避免在 Redis 内部产生费时的全集合遍历操作。

当然,我们不能忘了复杂度较高的 List 类型,它的两种底层实现结构:双向链表和压缩列
表的操作复杂度都是 O(N)。因此,我的建议是:因地制宜地使用 List 类型。例如,既然
它的 POP/PUSH 效率很高,那么就将它主要用于 FIFO 队列场景,而不是作为一个可以随
机读写的集合

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/54964.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

(文章复现)梯级水光互补系统最大化可消纳电量期望短期优化调度模型matlab代码

参考文献: [1]罗彬,陈永灿,刘昭伟等.梯级水光互补系统最大化可消纳电量期望短期优化调度模型[J].电力系统自动化,2023,47(10):66-75. 1.基本原理 1.1 目标函数 考虑光伏出力的不确定性,以梯级水光互补系统的可消纳电量期望最大为目标,函数…

uni-app:实现分页功能,单击行获取此行指定数据,更改行样式

效果&#xff1a; 分段解析代码 分页功能实现&#xff1a; 一、标签 1、搜索栏-模糊查询 <!-- 搜索框--><form action"" submit"search_wip_name"><view class"search_position"><view class"search"><…

uniapp 小程序实时且持续获取定位信息(全局设置一次)(单页面监听定位改变)(不采用定时器)

本篇文章实现了uniapp 微信小程序实时获取定位信息,小程序打开即可持续获取定位信息, 位置更新也会触发相关自定义事件 优点 只设置一次不采用定时器的方式无需多个页面调用单独页面若想获取当前位置是否变化 可单独设置监听,并调用不同逻辑事件 原理: 采用uniapp推出的: un…

数据结构——单链表OJ题

单链表OJ题 前言一、删除链表中等于给定值 val 的所有节点二、反转一个单链表三、返回链表的中间结点四、输出该链表中倒数第k个结点五、将两个有序链表合并六、链表的回文结构七、将链表分割成两部分八、找出第一个公共结点九、判断链表中是否有环总结 前言 在前面的博客中我…

中国农村程序员学习此【JavaScript教程】购买大平层,开上帕拉梅拉,迎娶白富美出任CEO走上人生巅峰

注&#xff1a;最后有面试挑战&#xff0c;看看自己掌握了吗 文章目录 在 Switch 语句添加多个相同选项从函数返回布尔值--聪明方法undefined创建 JavaScript 对象通过点号表示法访问对象属性使用方括号表示法访问对象属性通过变量访问对象属性给 JavaScript 对象添加新属性删除…

青大数据结构【2016】

一、单选 二、简答 3.简述遍历二叉树的含义及常见的方法。 4.简要说明图的邻接表的构成。 按顺序将图G中的顶点数据存储在一维数组中&#xff0c; 每一个顶点vi分别建立一个单链表&#xff0c;单链表关联依附顶点vi的边&#xff08;有向图为以vi为尾的弧&#xff09;。 邻接…

[LeetCode]只出现一次的数字相关题目(c语言实现)

文章目录 LeetCode136. 只出现一次的数字ⅠLeetCode137. 只出现一次的数字 IILeetCode260. 只出现一次的数字 IIILeetCode268. 丢失的数字 LeetCode136. 只出现一次的数字Ⅰ 题目: 给你一个 非空 整数数组 nums &#xff0c;除了某个元素只出现一次以外&#xff0c;其余每个元…

Pytorch深度学习-----神经网络的基本骨架-nn.Module的使用

系列文章目录 PyTorch深度学习——Anaconda和PyTorch安装 Pytorch深度学习-----数据模块Dataset类 Pytorch深度学习------TensorBoard的使用 Pytorch深度学习------Torchvision中Transforms的使用&#xff08;ToTensor&#xff0c;Normalize&#xff0c;Resize &#xff0c;Co…

【vue】 Tinymce 富文本编辑器 不想让上传的图片转换成base64,而是链接

前言&#xff1a;最近项目上需要使用富文本编辑器&#xff0c;觉得tinymce很不错就用了&#xff0c;具体怎么在项目中使用参考 【vue】 vue2 中使用 Tinymce 富文本编辑器 【vue】 Tinymce 数据 回显问题 | 第一次正常回显后面&#xff0c;显示空白bug不能编辑 这两天又遇到了…

“用户登录”测试用例总结

前言&#xff1a;作为测试工程师&#xff0c;你的目标是要保证系统在各种应用场景下的功能是符合设计要求的&#xff0c;所以你需要考虑的测试用例就需要更多、更全面。鉴于面试中经常会问“”如何测试用户登录“”&#xff0c;我们利用等价类划分、边界值分析等设计一些测试用…

iOS--frame和bounds

坐标系 首先&#xff0c;我们来看一下iOS特有的坐标系&#xff0c;在iOS坐标系中以左上角为坐标原点&#xff0c;往右为X正方向&#xff0c;往下是Y正方向如下图&#xff1a; bounds和frame都是属于CGRect类型的结构体&#xff0c;系统的定义如下&#xff0c;包含一个CGPoint…

【Docker】初识Docker以及Docker安装与阿里云镜像配置

目录 一、初识Docker 二、安装Docker 三、Docker架构 四、配置Docker镜像加速器 一、初识Docker Docker是一个开源的应用容器引擎&#xff0c;诞生于2013年&#xff0c;基于Go语言实现&#xff0c;dotCloud公司出品&#xff0c;Docker开源让开发者打包他们的应用以及依赖包到…

我的会议(会议通知)

前言: 我们在实现了发布会议功能&#xff0c;我的会议功能的基础上&#xff0c;继续来实现会议通知的功能。 4.1实现的特色功能&#xff1a; 当有会议要参加时&#xff0c;通过查询会议通知可以知道会议的内容&#xff0c;以及当前会议状态&#xff08;未读&#xff09; 4.2思路…

Python selenium对应的浏览器chromedriver版本不一致

1、chrome和chromedriver版本不一致导致的&#xff0c;我们只需要升级下chromedriver的版本即可 浏览器版本查看 //打开google浏览器直接访问&#xff0c;查看浏览器版本 chrome://version/ 查看chromedriver的版本 //查看驱动版本 chromedriver chromedriver下载 可看到浏…

在 Amazon EMR 上构建实时数据湖

前言 当公司业务发展遇到瓶颈时&#xff0c;业务分析师以及决策者们总会希望通过交叉分析大量的业务数据和用户行为数据&#xff0c;以解答“为什么利润会下滑&#xff1f;”“为什么库存周转变慢了&#xff1f;”等问题&#xff0c;最终整点“干货”出来从而促进业务发展。 …

一文了解JavaScript 与 TypeScript的区别

TypeScript 和 JavaScript 是两种互补的技术&#xff0c;共同推动前端和后端开发。在本文中&#xff0c;我们将带您快速了解JavaScript 与 TypeScript的区别。 一、TypeScript 和 JavaScript 之间的区别 JavaScript 和 TypeScript 看起来非常相似&#xff0c;但有一个重要的区…

替换linux的文泉驿正黑fonts-wqy-zenhei字体 替换linux默认中文字体

WSL 怎么替换 linux 的文泉驿正黑 fonts-wqy-zenhei 字体 WSL 怎么替换 linux 默认中文字体 在 wsl 中默认是没有 gnome 界面或者 xface 的&#xff0c;但是我需要使用 wsl 开发 electron 或者使用 chrome 浏览器。这个时候系统就会调用默认的系统字体了。 我使用的是 debian…

风辞远的科技茶屋:来自未来的信号枪

很久之前&#xff0c;有位朋友问我&#xff0c;现在科技资讯这么发达了&#xff0c;你们还写啊写做什么呢&#xff1f; 我是这么看的。最终能够凝结为资讯的那个新闻点&#xff0c;其实是一系列事情最终得出的结果&#xff0c;而这个结果又会带来更多新的结果。其中这些“得出”…

低代码开发平台源码:基于模型驱动,内置功能强大的建模引擎,零代码也能快速创建智能化、移动化的企业应用程序

管理后台低代码PaaS平台是一款基于 Salesforce Platform 的开源替代方案&#xff0c;旨在为企业提供高效、灵活、易于使用的低代码开发平台。低代码PaaS平台的10大核心引擎功能:1.建模引擎 2.移动引擎 3.流程引擎 4.页面引擎 5.报表引擎 6.安全引擎 7.API引擎 8.应用集成引擎 9…

SkyEye与Jenkins的DevOps持续集成解决方案

在技术飞速发展的当下&#xff0c;随着各行各业的软件逻辑复杂程度提升带来的需求变更&#xff0c;传统测试已无法满足与之相对应的一系列测试任务&#xff0c;有必要引入一个自动化、可持续集成构建的DevOps平台来解决此类问题。本文将主要介绍SkyEye与Jenkins的持续集成解决方…