ToB企服应用市场:ToB评测及商务社交产业平台

标题: 京东二面:Redis为什么快?我说Redis是纯内存访问的,然后他对我笑了笑。。 [打印本页]

作者: 惊雷无声    时间: 2024-5-15 13:35
标题: 京东二面:Redis为什么快?我说Redis是纯内存访问的,然后他对我笑了笑。。
引言

Redis是一个高性能的开源内存数据库,以其快速的读写速度和丰富的数据布局支持而闻名。作为一个轻量级、灵活的键值存储体系,Redis在各种应用场景下都展现出了惊人的性能优势。无论是作为缓存工具、会话管理组件、消息通报媒介,还是在实时数据处理任务和复杂的分布式体系架构中,Redis均扮演了至关重要的角色。而Redis为什么快的缘故原由也是我们尝尝遇见的高频口试问题。接下来我们就一起探究一下Redis快的缘故原由。
本文将深入探究Redis之所以快速处理大规模数据的缘故原由。我们将从Redis基于内存操作的特性、高效的内存数据布局、单线程模型、I/O多路复用技术、底层模型和优化技术、长期化机制以及网络通信协议等多个方面进行分析和讨论。通过深入了解Redis内部机制和性能优化技术,我们可以更好地明白Redis之所以快速的根本缘故原由,以及如安在实际应用中充实发挥其优势。

完全基于内存

Redis作为一种内存导向型数据库体系,其关键特性在于将所有数据实体,包括键值对及其相关的复杂数据布局,完全寄宿于内存之中。相较于依赖磁盘存储的传统数据库体系,Redis奥妙地运用内存的高速读写特性,显著提升了体系的相应速率与整体性能表现。
内存相对于磁盘具备无可比拟的读写速度优势,使得Redis能够即时、高效地处理数据存取。在读取操作层面,Redis无需经过耗时的磁盘I/O过程,只需在内存空间内敏捷定位所需数据,从而显著降低了访问延迟;而在写入操作时,Redis同样直接作用于内存地区,新数据能马上生效,仅在实行长期化策略时,比方RDB快照或AOF日记记录,数据才会被异步地或按需地同步至磁盘,以确保即使在体系重启后数据仍能得以恢复,但此过程并不会妨碍Redis在通例操作中维持其卓越的性能表现。
说到这,我们就会想到,一台服务器的内存不是无限的,相反的是比力告急的,Redis基于内存操作,那么Redis究竟是如安在有限内存空间中进行精致且高效的内存管理呢?
过期键删除

Redis支持为键设置过期时间(TTL),并且在键过期后会通过两种方式自动删除它们:
这两种方式结合起来,可以有效地管理和清理过期键,保证Redis的内存使用在合理范围内。同时,我们在日常开发中可以根据具体业务场景和需求调整过期策略的配置,以达到最佳的性能和内存使用率。
内存镌汰策略

内存镌汰策略是Redis用于释放内存空间的一种机制,当内存空间不足时(达到或超过了配置的maxmemory),Redis会根据预先设置的镌汰策略来选择要删除的键,从而释放内存空间。通过合理选择和配置内存镌汰策略,可以有效地管理内存使用,防止内存溢出,并保证体系的稳定性和性能。
常见的内存镌汰策略:
当Redis的内存使用达到配置的maxmemory限定时,就会触发内存镌汰策略,以释放内存空间。合理选择内存镌汰策略,并根据体系的需求设置maxmemory参数,可以有效地管理内存使用,保证体系的稳定性和性能。通过合理配置内存限定和内存镌汰策略,可以有效地管理Redis的内存使用,保证体系在内存空间不足时能够及时释放内存,避免因内存溢出而导致体系性能降落或者崩溃。
修改内存maxmemory只需要在redis.conf配置文件中配置maxmemory-policy参数即可。
内存碎片管理

内存碎片整理是指对Redis中的内存空间进行重新排列和整理,以减少内存碎片的数量和大小。内存碎片是指已分配但不再使用的内存块,这些内存块固然被标记为已分配,但实际上并未被有效使用,造成了内存的浪费。
在Redis中,由于数据的增删改查操作不停进行,会导致内存空间中出现大量的内存碎片。这些内存碎片固然单个很小,但假如积累起来会导致内存碎片化,降低内存使用率,影响体系的性能和稳定性。
为了办理内存碎片化的问题,Redis会定期进行内存碎片整理操作。内存碎片整理过程包括以下几个步调:
通过定期进行内存碎片整理操作,Redis可以保持内存空间的连续性,减少内存碎片化的程度,提高内存使用率,从而提高体系的性能和稳定性。但是,内存碎片整理过程可能会消耗一定的体系资源,尤其是在内存碎片较多的情况下。所以,通常情况下,Redis会选择在体系负载较低的时间进行碎片整理操作,以避免对体系性能产生不利影响。
高效的内存数据布局

Redis作为一个内存数据库体系,提供了丰富且高效的内存数据布局,包括字符串(String)、列表(List)、聚集(Set)、有序聚集(Sorted Set)、哈希(Hash)等。这些数据布局不光具有简单易用的特点,还能够在内存中高效地存储和操作数据,为Redis的快速性能提供了坚固的底子。

动态字符串

动态字符串是一种能够动态扩展长度的字符串实现方式。在许多编程语言和数据布局中都有类似的实现,如C语言中的动态数组(dynamic array)。而SDS是Redis中的一种简单动态字符串布局,它是一种动态大小的字节数组,用于存储和操作字符串数据。SDS是Redis内部数据布局的底子,也是字符串数据布局的底层实现。它的布局如下:
  1. /*
  2. * redis中保存字符串对象的结构
  3. */
  4. struct sdshdr {
  5.     //用于记录buf数组中使用的字节的数目,和SDS存储的字符串的长度相等
  6.     int len;
  7.     //用于记录buf数组中没有使用的字节的数目
  8.     int free;
  9.     //字节数组,用于储存字符串
  10.     char buf[]; //buf的大小等于len+free+1,其中多余的1个字节是用来存储’\0’的
  11. };
复制代码
在C语言中传统字符串是使用长度为N+1的字符数组来表现长度为 的字符串,并且字符串数组的末了一个元素总是空字符'\0'。

假如我们想要获取上述CODERACADEMY的长度,我们需要从头开始遍历,直到碰到 '\0' 为止。
而Redis的SDS的数据布局使用一个len字段记录当前字符串的长度,使用free表现空闲的长度。想要获取长度只需要获取len字段即可。

我们可以看出C语言获取字符串长度的时间复杂度为O(N),而SDS获取字符串长度的时间复杂度为O(1)。除此之外,SDS相对于C语言字符串还有如下区别:
特性C语言字符串SDS类型静态字符数组动态字符串布局内存管理需手动分配和释放内存自动扩展和释放内存存储空间需要提前预留富足的空间根据需要动态调整大小长度计算需要遍历整个字符串计算长度O(1)复杂度直接获取字符串长度二进制安全不二进制安全二进制安全缓冲区溢出保护不提供缓冲区溢出保护提供缓冲区溢出保护操作复杂度操作复杂度随字符串长度增加而增加操作复杂度不受字符串长度影响可拓展性不易扩展,需要手动处理内存扩展自动扩展,支持动态调整大小细说下来,SDS相对于C语言字符串有如下优点:这些优点使得SDS在Redis中被广泛应用于存储和操作字符串数据,为Redis的高性能和高可靠性提供了坚固的底子。
双端链表

Redis中的双端链表是一种经过优化的数据布局,用于存储有序的元素聚集。它具有双向链接的特性,每个节点都包罗指向前一个节点和后一个节点的指针。

双端链表中的节点是链表的基本构建单元,它存储了链表中的数据元素以及指向前一个节点和后一个节点的指针。在Redis中,双端链表节点的定义通常如下:
  1. typedef struct listNode {
  2.     struct listNode *prev;  // 指向前一个节点的指针
  3.     struct listNode *next;  // 指向后一个节点的指针
  4.     void *value;            // 存储的数据元素
  5. } listNode;
复制代码
双端链表中的节点包罗了以下几个关键属性:
通过这些属性,双端链表节点构成了链表的基本组成部分,它们通过prev和next指针连接在一起,形成了双向链接的链表布局。
对于链表中形貌链表整体属性的元数据,它的布局如下:
  1. typedef struct list {
  2.     listNode *head;  // 头节点指针
  3.     listNode *tail;  // 尾节点指针
  4.     unsigned long len;  // 链表长度
  5.     // 其他字段...
  6. } list;
复制代码
从布局中可以看出元数据中还有两个特别的节点:头节点(head node)和尾节点(tail node),它们分别位于链表的头部和尾部。而他们的作用如下:
在Redis中,通常会使用头节点和尾节点来表现双端链表的起始位置和结束位置,以方便对链表进行操作。Redis中的双端链表常见操作如下:
通过头节点和尾节点,可以方便地对双端链表进行头部插入、尾部插入、头部删除、尾部删除等操作,从而实现了对双端链表的高效操作。
除了上述头尾节点以外,链表的元数据中还有len参数,这个参数用于记录链表的当前长度。每当链表中添加或删除节点时,Redis会相应地更新len字段的值,以反映链表的当前长度。这个参数与SDS里类似,获取链表长度时不消再遍历整个链表,直接拿到len值就可以了,这个时间复杂度是 O(1)。
压缩列表

Redis中的压缩列表(ziplist)是一种特别的数据布局,用于存储列表和哈希数据类型中的元素。压缩列表通过将多个小的数据单元压缩在一起,以节省内存空间,并提高访问服从。

对于压缩列表,它的重要作用如下:
字典

在Redis中,字典(dictionary)是一种用于存储键值对数据的数据布局,也称为哈希表(hash table)。字典是Redis中最常用的数据布局之一,具有快速查找、动态调整大小、哈希辩论处理、迭代器支持等特点,适用于各种数据存储和操作需求,实现键值对存储和快速查找。
字典以键值对的情势存储数据,每个键都与一个值相关联。在Redis中,键和值都可以是任意类型的数据,如字符串、整数、列表或哈希表。
字典使用哈希表实现,具备快速查找的特性。通过将键映射到哈希表的索引位置,字典能以常数时间复杂度(O(1))内查找、插入和删除键值对,即使在大型数据集中也能保持高效。
此外,字典支持动态调整大小,随着键值对数量的变革,能自动扩展或紧缩内存空间,以顺应数据量的变革。
在存储数据时,假如产生了哈希辩论,字典可以采用开放寻址法或链表法等策略,根据哈希表的大小和负载因子选择合适的辩论办理方法,确保查找性能高效。
跳跃表

跳跃表(Skip List)是一种基于链表的数据布局,它使用多级索引来加快查找操作,类似于均衡树,但实现起来更加简单,具有较好的平均查找性能。在Redis中,跳跃表用于有序聚集(Sorted Set)数据类型的实现,提供了高效的有序数据存储和检索功能。

跳跃表通过维护多级索引,每个级别的索引都是原始链表的子集,用于快速定位元素。每个节点在不同级别的索引中都有一个指针,通过这些指针,可以在不同级别上进行快速查找,从而提高了查找服从。

跳跃表的平均查找性能为O(log n),与均衡树相当,但实现起来更加简单。跳跃表通过多级索引来实现快速查找,使得查找时间随着数据量的增加而呈对数增长。但是跳跃表的空间复杂度相对较高,由于它需要额外的空间来维护多级索引。不外跳跃表的空间占用通常是合理的,且具有可控性,可以根据实际需求调整级别和索引节点的数量,以均衡空间和性能的需求。
除此之外,跳跃表支持动态调整大小,可以根据实际需要自动扩展或紧缩内存空间。当有序聚集中的元素数量增加时,跳跃表会动态地增加级别和索引节点,以提高查找服从;当元素数量减少时,可以紧缩跳跃表的大小,以节省内存资源。并且跳跃表的插入和删除操作具有较高的服从,通过维护多级索引,可以在O(log n)的时间复杂度内完成插入和删除操作。
单线程模型

Redis中的单线程模型是指Redis在其核心数据处理部分采用单一的主线程来实行网络IO操作、接收客户端命令请求、实行命令操作以及返回结果。Redis服务端的网络IO和键值对读写操作都由一个线程同一负责,而诸如长期化、集群数据同步等任务则是由其他线程来实行。在单线程模型下,Redis服务器是单线程运行的,即每个客户端的请求都是依次顺序实行的。
而使用单线程所带来的好处:
在日常开发中,我们通常会使用并发编程来提高服务的吞吐量。这时,我们可能会产生一个疑问:Redis的单线程模型是否能够充实使用CPU资源呢?
实际上,由于Redis是基于内存的操作,使用Redis时,CPU很少会成为瓶颈。相反,Redis重要受限于服务器内存和网络带宽。比方,在典型的Linux体系上,通过使用pipelining技术,Redis能够实现较高的吞吐量,每秒可以处理大量的请求。因此,假如应用程序重要使用O(N)或O(log(N))的命令,它几乎不会对CPU资源造成过多的负载。综上所述,考虑到单线程模型的实现简单且CPU很少成为瓶颈,因此采用单线程方案是合理的选择。
单线程模型限定了Redis的并发能力。由于只有一个线程在处理请求,无法充实使用多核处理器的性能优势,所以可能到达服务端的请求不可能被立即处理。那么Redis是如何保证单线程的资源使用率和处理服从呢?
IO多路复用技术
Redis通过使用IO多路复用技术(如epoll、kqueue或select等),在一个线程内同时监听多个socket连接,当有网络事故发生时(如读写就绪),再逐一处理。这样可以处理大量并发连接,并在单线程中高效地调度网络事故,使得单线程也能应对高并发场景。所以Redis服务端,整体来看,就是一个以事故驱动的程序,它的操作都是基于事故的方式进行的。Redis的事故驱动架构如图:

Redis的事故驱动架构是一种基于非壅闭I/O多路复用技术计划的高效处理并发请求的机制。在Redis中,事故驱动架构通过监听和处理各种网络I/O事故以及定时事故,使得Redis服务端能够在一个线程内高效地服务于多个客户端连接,并实行相关的命令操作。
事故驱动架构重要由以下几个组成部分构成:
Redis默认使用的IO多路复用技术确实是epoll。其重要优点如下:
而对于Redis中计划的事故重要分为两个大类:
通过事故驱动架构,Redis能够在一个线程内并发处理大量客户端请求,而无需为每个客户端创建独立的线程。此外,由于Redis的高效内存管理、数据布局优化和单线程模型,避免了多线程情况下的锁竞争和上下文切换开销,从而实现了极高的吞吐量和相应速度。
在Redis 6.x版本中,固然引入了多线程处理网络IO的部分,但核心命令实行依然保持单线程事故驱动的模型,以维持Redis原有的性能优势。
IO多路复用模型

IO多路复用的核心在于内核关注的是应用程序的文件形貌符而非直接监控连接本身。客户端运行时产生的不同事故类型的套接字操作,会被内核捕获。在服务器端,I/O多路复用机制负责收集这些事故并将它们参加事故队列,随后通过文件事故分发器分发至对应事故处理器进行处理。
以Redis为例,在其单线程模型下,内核不间断地监测所有客户端socket的连接请求和数据传输状况。只要检测到任何socket上有待处理的动作,便会立即将控制权转交给Redis线程。这样一来,只管仅依靠单线程,Redis仍能有效地处理多个并发的IO流。
select/epoll等IO多路复用技术提供了一种基于事故触发的回调模式,每当有不同事故发生时,Redis能够敏捷调用相应的事故处理器,始终保持在处理事故的状态,从而提升了其相应速度。

由于Redis线程并不会由于等待某个特定socket的IO操作完毕而停滞,它可以流畅地在多个客户端间切换,即时相应每个客户端的不同请求,从而实现在单线程情况下对大量并发连接的有效处理和高并发性能。
简单高效的通信协议

Redis Cluster在集群内部通信中借鉴了Gossip协议的理念,采用了一种基于Gossip风格的消息传播机制。这种机制能够有效地将集群状态和节点信息在集群中的各个节点间进行快速传播和同步。类比于流行病的传播模型,Gossip协议允许节点随机选择邻居节点进行通信,从而在全网状布局中快速传播更新。
Redis Cluster、Consul和Apache Cassandra平分布式体系都采用了Gossip协议或者类似的机制来维护集群的康健状态和一致性。通过Gossip协议,节点们可以高效地共享和更新集群的元数据,如节点参加、离开、故障转移等信息。
然而,纯粹的Gossip协议在实践中可能存在信息冗余的问题,即已接收到某一信息的节点在后续的传播中可能会收到相同的信息。为了避免这种冗余和提高通信服从,这些体系通常会对Gossip协议进行优化,比方在节点间记录已知信息的状态,避免重复传播已知的更新。即便如此,Gossip协议仍然是在大规模分布式体系中实现高可用性和强一致性的有效手段,其高效性体现在只需局部通信即可逐渐达成全局一致性,同时具备精良的扩展性和容错性。
总结

末了,我们来总结一下,如安在口试中回答Redis为什么快的缘故原由:

本文已收录于我的个人博客:码农Academy的博客,专注分享Java技术干货,包括Java底子、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中间件、架构计划、口试题、程序员攻略等

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4