单线程的Redis有哪些慢动作?
持续原创输出,点击上方蓝字关注我
目录
前言 为什么 Redis 这么火? 键和值的保存形式? 为什么哈希表操作变慢了? 集合的操作效率? 有哪些数据结构? 不同操作的复杂度?
总结
前言
现在一提到Redis
的第一反应就是快
、单线程
,但是Redis
真的快吗?真的是单线程吗?
你有没有深入了解一下Redis
,看看它的底层有哪些"慢动作"呢?
为什么 Redis 这么火?
Redis
作为一个内存数据库,它接收一个key
到读取数据几乎是微妙级别
,一个字快
诠释了它火的原因。另一方面就归功于它的数据结构了,你知道Redis
有哪些数据结构吗?
很多人可能会说不就是String
(字符串)、List
(列表)、Hash
(哈希)、Set
(集合)和 Sorted Set
(有序集合)这五种吗?我想大家可能有一种误区,我说的是底层数据结构,而你说仅仅是数据的保存形式而已。
那么Redis
底层有哪几种数据结构呢?和几种数据保存形式的关系又是什么呢?别着急,先通过一张图了解下,如下图:
通过上图可以知道只有String
对应的是一种数据结构,其他的数据类型对应的都是两种数据结构,我们通常将这四种数据类型称为集合类型,它们的特点是「一个键对应了一个集合的数据」。
既然数据本身是通过数据结构保存的,那么键和值是什么保存形式呢?
键和值的保存形式?
为了实现键和值的快速访问,Redis
使用的是哈希表
来存放键,使用哈希桶
存放值。
一个
哈希表
其实就是一个数组,数组的每个元素称之为哈希桶
。
所以,一个哈希表是由多个哈希桶组成,每个哈希桶中保存了键值对数据。
哈希桶
中保存的并不是值,而是指向值的指针
。
这也解释了为什么哈希桶
能够保存集合类型的数据了,也就是说不管是String
还是集合类型,哈希桶
保存的都是指向具体的值的指针
,具体的结构如下图:
从上图可以看出,每个entry
中保存的是*key
和*value
分别指向了键和值,这样即使保存的值是集合类型也能通过指针 *value
找到。
键是保存在哈希表中,哈希表的时间复杂度是
O(1)
,也就是无论多少个键,总能通过一次计算就找到对应的键。
但是问题来了,当你往Redis
中写入大量的数据就有可能发现操作变「慢」了,这就是一个典型的问题:「哈希冲突」。
为什么哈希表操作变慢了?
既然底层用了哈希表,则哈希冲突是不可避免的,那什么是哈希冲突呢?
Redis
中的哈希冲突则是两个或者多个key
通过计算对应关系,正好落在了同一个哈希桶中。
这样则导致了不同的key
查找到的值是相同的,但是这种问题在Redis
中显然是不存在的,那么Redis
用了什么方法解决了哈希冲突呢?
Redis
底层使用了链式哈希
的方式解决了哈希冲突,即是同一个哈希桶中的多个元素用一个链表
保存,他们之间用指针*next
相连。
此时的哈希表和链式哈希的结构如下图:
从上图可以看到,entry1
、entry3
、entry3
都保存在哈希桶 1 中,导致了哈希冲突。但是此时的entry1
中的*next
指针指向了entry2
,同样entry2
中的*next
指针指向了entry3
。这样下来即使哈希桶中有很多个元素,也能通过这样的方式连接起来,称作哈希冲突链
。
这里存在一个问题:链表的查询效率很低,如果哈希桶中元素很多,查找起来会很「慢」,显然这个对于
Redis
来说是不能接受的。
Redis
使用了一个很巧妙的方式:「渐进式 rehash」。那么什么是渐进是rehash
呢?
想要理解渐进式rehash
,首先需要理解下的rehash
的过程。
rehash
也就是增加现有的哈希桶数量,让逐渐增多的entry
元素能在更多的桶之间分散保存,减少单个桶中的元素数量,从而减少单个桶中的冲突。
为了使rehash
操作更高效,Redis
默认使用了两个全局哈希表:哈希表1
和哈希表2
。一开始,当你刚插入数据时,默认使用哈希表1
,此时的哈希表2
并没有被分配空间。随着数据逐步增多,Redis
开始执行rehash
,这个过程分为三步:
给 哈希表2
分配更大的空间,例如是当前哈希表1
大小的两倍把 哈希表1
中的数据重新映射并拷贝到哈希表2
中释放 哈希表1
的空间。
以上这个过程结束,就可以释放掉哈希表1
的数据而使用哈希表2
了,此时的哈希表1
可以留作下次的rehash
备用。
此时这里存在一个问题:
rehash
整个过程的第 2 步涉及到大量的拷贝,一次性的拷贝数据肯定会造成线程阻塞,无法服务其他的请求。此时的Redis
就无法快速访问数据了。
为了避免一次性拷贝数据导致线程阻塞,Redis
使用了渐进式rehash
。
渐进式rehash
则是rehash
的第 2 步拷贝数据分摊到每个请求中,Redis 仍然正常服务,只不过在处理每次请求的时候,从哈希表1
中索引1
的位置将所有的entry
拷贝到哈希表2
中,下一个请求则从索引1
的下一个的位置开始。
通过渐进式 rehash 巧妙的将一次性开销分摊到各个请求处理的过程中,避免了一次性的耗时操作。
此时可能有人提出疑问了:「如果没有请求,那么Redis
就不会rehash
了吗?」
Redis
底层其实还会开启一个定时任务,会定时的拷贝数据,即使没有请求,rehash
也会定时的在执行。
集合的操作效率?
如果是string
,找到哈希桶中的entry
则能正常的进行增删改查了,但是如果是集合呢?即使通过指针找到了entry
中的value
,但是此时是一个集合,又是一个不同的数据结构,肯定会有不同的复杂度了。
集合的操作效率肯定是和集合底层的数据结构相关,比如使用哈希表实现的集合肯定要比使用链表实现的结合访问效率要高。
接下来就来说说集合的底层数据结构和操作复杂度。
有哪些数据结构?
本文的第一张图已经列出了集合的底层数据结构,主要有五种:整数数组
、双向链表
、哈希表
、压缩列表
和跳表
。
以上这五种数据结构都是比较常见的,如果读者不是很了解具体的结构请阅读相关的书籍,我就不再赘述了。
五种数据结构按照查找时间的复杂度分类如下:
数据结构 | 时间复杂度 |
---|---|
哈希表 | O(1) |
跳表 | O(logN) |
双向链表 | O(N) |
压缩链表 | O(N) |
整数数组 | O(N) |
不同操作的复杂度?
集合类型的操作类型很多,有读写单个集合元素的,例如 HGET
、HSET
,也有操作多个元素的,例如SADD
,还有对整个集合进行遍历操作的,例如 SMEMBERS
。这么多操作,它们的复杂度也各不相同。而复杂度的高低又是我们选择集合类型的重要依据。
下文列举了一些集合操作的复杂度,总共三点,仅供参考。
1. 单元素操作由底层数据结构决定
每一种集合类型对单元素的增删改查操作这些操作的复杂度由集合采用的数据结构决定。例如,HGET
、HSET
和HDEL
是对哈希表做操作,所以它们的复杂度都是O(1)
;Set
类型用哈希表作为底层数据结构时,它的SADD
、SREM
、SRANDMEMBER
复杂度也是 O(1)
。
有些集合类型还支持一条命令同时对多个元素的操作,比如Hash
类型的HMGET
和HMSET
。此时的操作复杂度则是O(N)
。
2. 范围操作非常耗时,应该避免
范围操作是指集合类型中的遍历操作,可以返回集合中的所有数据或者部分数据。比如List
类型的HGETALL
和Set
类型的SMEMBERS
,这类操作的复杂度为O(N)
,比较耗时,应该避免。
不过Redis
提供了Scan
系列操作,比如HSCAN
、SSCSCAN
和ZSCAN
,这类操作实现了渐进式遍历,每次只返回有限数量的数据。这样一来,相比于HGETALL
、SMEMBERS
这类操作来说,就避免了一次性返回所有元素而导致的 Redis
阻塞。
3. 统计操作通常比较高效
统计操作是指对集合中的所有元素个数的记录,例如LLEN
和SCARD
。这类操作复杂度只有O(1)
,这是因为当集合类型采用压缩列表、双向链表、整数数组这些数据结构时,这些结构中专门记录了元素的个数统计,因此可以高效地完成相关操作。
总结
Redis
之所以这么快,不仅仅因为全部操作都在内存中,还有底层数据结构的支持,但是数据结构虽好,每种数据结构也有各种「慢」的情况,Redis
结合各种数据结构的利弊,完善了整个运行机制。
单线程的Redis有哪些慢动作?的更多相关文章
- 单线程的redis为什么吞吐量可以这么大
原文:单线程的redis为什么吞吐量可以这么大 一.Redis的高并发和快速原因 1.redis是基于内存的,内存的读写速度非常快: 2.redis是单线程的,省去了很多上下文切换线程的时间: 3.r ...
- redis和memcached有什么区别?redis的线程模型是什么?为什么单线程的redis比多线程的memcached效率要高得多(为什么redis是单线程的但是还可以支撑高并发)?
1.redis和memcached有什么区别? 这个事儿吧,你可以比较出N多个区别来,但是我还是采取redis作者给出的几个比较吧 1)Redis支持服务器端的数据操作:Redis相比Memcache ...
- 性能测试 | 理解单线程的Redis为何那么快?
前言 Redis是一种基于键值对(Key-Value)的NoSQL数据库,Redis的Value可以由String,hash,list,set,zset,Bitmaps,HyperLogLog等多种数 ...
- 为什么单线程的Redis这么快?
一. Redis简介 Redis是一个开源的内存中的数据结构存储系统,它可以用作数据库.缓存和消息中间件. 它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(li ...
- 【*】单线程的redis为什么吞吐量可以这么大
一.Redis的高并发和快速原因 1.redis是基于内存的,内存的读写速度非常快: 2.redis是单线程的,省去了很多上下文切换线程的时间: 3.redis使用多路复用技术,可以处理并发的连接.非 ...
- 单进程单线程的Redis如何能够高并发
redis快的原因: 1.纯内存操作2.异步非阻塞 IO 参考文档: (1)http://yaocoder.blog.51cto.com/2668309/888374 (2)http://www.cn ...
- 单线程的redis为什么达到每秒万级的处理速度?
纯内存访问,redis将所有数据都放在内存中,内存响应时间大约为100纳秒,这是redis达到每秒万级级别访问的重要基础. 非阻塞IO,redis使用epoll作为IO多路复用技术的实现,再加上red ...
- 为什么单线程的Redis却能支撑高并发
Redis的高并发和快速原因 redis是基于内存的,内存的读写速度非常快: 核心是基于非阻塞的IO多路复用机制: redis是单线程的,反而省去了很多上下文切换线程的时间: 为什么Redis是单线程 ...
- 单线程的REDIS为什么这么快?
REDIS是单线程处理所有请求,和一般经典实际上推荐的方式相反,那么单线程串行处理,为什么依然能够做到很快呢?知乎上的一个答案如下,其中线程切换和锁不是性能主要影响因素的观点和一般的答案都不同: 作者 ...
随机推荐
- 阿里云ecs,rds,redis优惠套餐
阿里云ECS优惠套餐 阿里云RDS优惠套餐 阿里云REDIS优惠套餐 阿里云REDIS集群版 阿里云短信资源包
- JDK源码阅读-------自学笔记(二十五)(java.util.Vector 自定义讲解)
Vector 向量 Vector简述 1).Vector底层是用数组实现的List 2).虽然线程安全,但是效率低,所以并不是安全就是好的 3).底层大量方法添加synchronized同步标记,sy ...
- KVM虚拟化管理平台WebVirtMgr部署及使用
KVM虚拟化管理平台WebVirtMgr部署及使用 需求: 公司机房有一台2U的服务器(64G内存,32核),由于近期新增业务比较多,测试机也要新增,服务器资源十分有限.所以打算在这台2U服务器上 ...
- mysql在DOS环境下操作的命令
管理员运行cmd,执行启动mysql命令:net start MySQL版本号 登录数据库:mysql -u root -p 输入密码 创建数据库:drop database if exists 数据 ...
- Codeforces Round #679 (Div. 2, based on Technocup 2021 Elimination Round 1)
考场上只做出来四道,第二天一早就写出来了E,蛮绝望的. A Finding Sasuke 水构造 #include <cstdio> #include <algorithm> ...
- 监控制图OxyPlot组件的下载与安装
1.在工具(T)-NuGet包管理器(N)-管理解决方案的NuGet程序包(N),打开组件管理界面 2.切换到浏览窗口,安装以下三个窗口组件即可 3.OxyPlot文档手册 https://oxypl ...
- nginx反向代理三台web服务器,实现负载均衡
修改nginx.conf #在http和server之间加入这个模块 upstream guaji{ server 127.0.0.1:8080; server 127.0.0.2:8080; ser ...
- [UOJ 275/BZOJ4737] 【清华集训2016】组合数问题 (LUCAS定理的运用+数位DP)
题面 传送门:UOJ Solution 这题的数位DP好蛋疼啊qwq 好吧,我们说回正题. 首先,我们先回忆一下LUCAS定理: \(C_n^m \equiv C_{n/p}^{m/p} \times ...
- [Luogu P4178]Tree (点分治+splay)
题面 传送门:https://www.luogu.org/problemnew/show/P4178 Solution 首先,长成这样的题目一定是淀粉质跑不掉了. 考虑到我们不知道K的大小,我们可以开 ...
- http twisted
Sunday, September 30th, 2007 Twisted的WEB开发 作者: gashero <harry.python@gmail.com> 目录 1 简介 2 ...