redis 有多快
卡工程师模因。大多数程序员都参与业务开发,但是学*底层知识会不会感到困惑呢?
Redis是一个基于内存操作的高性能K-V数据库。官方测试报告显示,一台机器可以支持10w/s左右的QPS。我不太确定为什么Redis 表现这么好。
完全基于内存实现
磁盘调用堆栈图
内存操作
如您所知,内存访问速度比硬盘访问速度快得多。我们来比较一下数据库(硬盘)和Redis(内存)。一种操作对应于磁盘,一种操作对应于内存。两者的访问速度都要高出几个数量级。
也许你没有这个非凡的概念。这可是整整1000次啊!现在大家都知道了。
高效的数据结构
1、简单的动态字符串SDS是对C语言字符数组的封装,类似于GO语言中的切片,添加了一些额外的参数,以方便更快的字符串操作,也是对底层数组的封装。
处理字符串长度
该图显示了C 语言中字符串的存储方式。如果想要获取'Redis'的长度,需要从头开始追踪,直到找到'\0'。
Redis使用len字段来记录当前字符串长度。要获取长度,只需获取len 字段即可。以前遍历的时间复杂度是O(n),但是有了Redis就可以O(1)计算,速度快很多。
内存重新分配
在C 语言中,更改字符串会重新分配内存。更改发生得越频繁,分配内存的频率就越高。内存分配会消耗性能,因此性能下降是不可避免的。
由于Redis频繁进行字符串修改操作,这种分配内存的方式显然是不合适的。因此,SDS实施了两种优化策略。
预分配空间
当修改SDS 以增加空间时,除了所需的空间分配之外,还会分配额外的未使用空间。 GO语言中的切片也会分配保留空间,以防止频繁分配空间。
具体赋值规则为: SDS更改后,如果len的长度小于1M,则额外分配与len相同长度的未使用空间。如果修改的长度超过1M,则分配1M的已用空间。
释放懒惰空间
当然,分配空间对应于释放空间。
当SDS缩短时,多余的内存空间不会被回收,而是使用空闲空间来记录多余的空间。任何后续更改都会直接使用可用空间,从而减少内存分配。
二进制安全
您已经知道Redis可以存储多种数据类型,二进制数据也不例外。但是,二进制数据不是常规字符串格式,可能包含特殊字符,例如“\0”。
前面说过,C中的字符串遇到'\0'就终止,'\0'之后就无法读取任何数据。但是,SDS 使用len 的长度来确定字符串的结尾。
2. 双端链表
列表列表通常用作队列或堆栈。队列和栈的特点是先进先出和先进后出。双向链表很好地支持了这些功能。
前节点和后节点
链表中的每个节点都有两个指针:prev指向前一个节点,next指向下一个节点。这样,我们就可以在O(1) 的时间复杂度内检索前一个和后一个节点。
头节点和尾节点
你可能注意到了,头节点有两个参数,head和tail,分别指的是头节点和尾节点。这样的设计可以将双端节点的处理时间复杂度降低到O(1),非常适合队列和堆栈。您可以同时从两端迭代链表。
列表长度
头节点还有一个和上面介绍的SDS类似的参数len,用来记录链表的长度。所以当我们想要获取链表的长度时,我们不需要遍历整个链表,这次直接获取len值即可。复杂度为O(1)。不需要每次遍历的时候都获取长度。
3. 压缩列表
我们已经熟悉双端链表。我不知道你是否注意到这个问题。这是当您在链表节点中存储少量数据(例如字节)时。然后,我们需要相应地存储头节点、前指针、后指针等附加数据。
这样既浪费空间,又容易因重复申请和释放而导致内存碎片。这样内存使用效率太低了。
这就是压缩列表登场的地方。
专门编码和设计,以提高内存使用效率。所有操作都是通过指针和解码后的偏移量来执行的。
另外,压缩列表的内存是连续分配的,因此扫描速度非常快。
4. 词典
Redis是一个K-V数据库,所有的键值都存储在字典中。
字典也称为哈希表,但对此没有太多可说的。我们都熟悉哈希表的属性,它允许我们在O(1) 时间内检索和插入相关值。
5. 跳跃平台
跳表作为Redis中特有的一种数据结构,在链表的基础上添加了多级索引,以提高搜索效率。
这是跳转列表的简单示意图。每个级别都有一个有序链表,最底层的链表包含所有元素。这样,跳转表就可以支持以O(logN)的时间复杂度找到对应的节点。
下图展示了跳转表的实际存储结构。与其他数据结构类似,头节点上记录了相应的信息,以减少不必要的系统开销。
单线程模型
单线程是指Redis键值对读写指令的执行是单线程的。
Redis中的单线程是指Redis中的网络IO(从6.x版本开始,网络IO使用多线程),其中键值对指令的读写都是在一个线程中进行。
单线程的优点是什么?
线程创建不会消耗任何性能。避免了由于上下文切换而导致的CPU消耗,而没有多线程切换的开销。避免了线程之间的争用问题,例如加锁、释放锁、死锁等,也不必考虑各种加锁问题。代码更清晰,处理逻辑更简单。
多线程的缺点
使用多线程后,如果没有适当的系统设计,线程数量就会增加,增加更多的线程可能会导致系统吞吐量的增加很少,甚至减少2。它需要完成一系列的任务,这是一个非常消耗资源的操作。 Redis的所有操作都是基于内存操作,很少有计算操作消耗CPU。瓶颈在于Redis的运算内存的速度。从网络IO的角度来看,redis中的网络IO接收事件绝对是一个需要改进的地方。这是因为所有客户端对redis的操作最终都会被redis的网络模块接受,所以网络IO利用率对于redis来说很高。非常有必要。
Redis在4.0版本中引入了多线程来执行一些异步操作。这主要是为了异步运行这些命令,避免单线程事件循环中的阻塞(添加了非阻塞命令,例如: UNLINK、FLUSHALL ASYNC、FLUSHDB ASYNC 等)。
多线程IO是在redis v6.0中引入的。它仅用于读写网络数据和处理协议分析,但命令执行仍然是单线程的。
I/O 多路复用
Redis 使用I/O 复用技术来并发处理连接。我使用的是epoll + 我自己实现的一个简单的事件框架。
epoll中所有的读、写、退出、连接都会转化为事件,利用epoll的复用能力,你就不会在IO上浪费时间了。
Redis 线程不再阻塞特定侦听或连接的套接字。也就是说,它不会阻止处理特定的客户端请求。这使得Redis 可以同时连接并处理来自多个客户端的请求,从而提高并发性。
合理的数据编码方式
对于每种数据类型,底层支持可能是多种数据结构。使用哪种数据结构涉及到编码转换问题。
现在我们来看看不同的数据类型是如何编码和转换的。
String:如果存储的是数字,则使用int编码。对于非数字值,使用原始编码。
列表:当字符串长度和元素数量小于一定范围时,使用ziplist编码。如果不满足条件,就会转为链表编码。
哈希:哈希对象存储的键值对的键值字符串的长度比特定值的键值对的长度短。
Set:将元素存储为整数,当元素数量小于一定范围时使用哈希表编码。
zset:如果zset对象中存储的元素数量小于一定值且成员长度小于一定值,则使用ziplist编码。如果不满足条件,则使用跳表编码。
总结
纯内存操作一般都是简单的访问操作,花费的时间主要集中在IO上,导致读取速度更快。整个Redis是一个全局哈希表,其时间复杂度为O(1)。 Redis 使用非阻塞IO。将高效率与IO 多路复用进行比较,使用单个线程轮询描述符,并使用专有的事件分隔符将数据库打开、关闭、读取和写入转换为事件。使用单线程模型保证了每个操作的原子性,减少了线程上下文切换和争用。 Redis全程使用哈希结构,提供更快的读取速度。还有一些专门优化数据存储的数据结构,例如用于存储压缩短数据的压缩表。另一个例子是使用有序数据结构来提高速度。阅读速度。根据实际存储的数据类型选择不同的编码。
最后
Redis 6.0中的多线程仅使用多线程来处理网络请求,但数据读写命令仍然由单线程处理。
预计未来版本将会使用多线程来进行redis数据读写命令,您认为这样合适吗?
版权声明:本文转载于网络,版权归作者所有。如有侵权,请联系本站编辑删除。