最全Redis面试题,直接把这些甩给面试官!
Redis 是什么
Redis 是一个开源的基于内存的数据库,它被广泛用于缓存、消息队列、会话存储以及数据存储等各种用途。Redis以键值对(key-value)的形式存储数据,并支持多种数据结构,如字符串、哈希表、列表、集合和有序集合等。
相比其他数据库有以下优点:
- 快速:Redis能读的速度是110000次/s,写的速度是81000次/s。
- 支持丰富数据类型:Redis支持二进制安全的字符串、哈希表、列表、集合、有序集合等数据结构。
- 原子性:Redis的所有操作都是原子性的,而且Redis还能对多个操作打包成原子性的,通过这个功能可以实现批量插入。
- 丰富的特性:Redis还支持push/pop、add/remove及并集、交集等操作,这些操作都是原子性的,所以是很高效的。还能进行排序。
由于 Redis 对数据的读写操作都是在内存中完成,因此读写速度非常快,常用于缓存,消息队列、分布式锁等场景。
Redis 有哪些应用场景,能举例详细说明吗?
Redis在多种应用场景中都有广泛的应用,下面是一些常见的应用场景,:
- 缓存系统:Redis可以用来作为应用程序和数据库之间的缓存层,存储热点数据或者频繁访问的数据,提高数据查询速度,减轻数据库压力。例如,将用户的个人信息、商品信息等存储在 Redis 中,在用户访问时先查询 Redis,若缓存中没有则查询数据库,并将结果存入 Redis 以供后续使用。
- 计数器:Redis 的原子操作可以实现各种计数器,如网站访问量计数、社交媒体的点赞数、文章的点击量等。可以使用 INCR 命令对键值对执行自增操作,或者使用 DECR 命令进行自减操作。
- 消息队列:通过 Redis 的 List 数据结构可以实现简单的消息队列系统。使用 LPUSH 向队列中添加消息,RPOP 从队列中取出消息,并进行处理。或使用阻塞式操作 BRPOP/BLPOP 等待队列中的消息。
- 排行榜:利用 Redis 的有序集合(Sorted Set)可以维护实时的排行榜数据,如游戏分数排行、销售额排行等。通过添加成员和对应分数到有序集合,然后使用ZREVRANGE获取前N名的成员。
- 实时分析:Redis 的高性能和数据结构可以用于监视和分析实时数据,比如实时显示访客来源、在线用户数量、实时热点等。
- Session共享:在分布式系统中,为了实现 Session 的读取和写入效率,通常会将 Session 数据存储在 Redis 中,各个应用服务器可以共享这些 Session 数据。
- 分布式锁:Redis 的分布式性质和原子性操作使其适合用作分布式锁的实现。这可以帮助解决分布式系统中的并发访问问题。
- 限流和防刷:Redis 可以用来实现请求限流和防刷策略,以确保系统不会受到恶意请求或大量请求的影响。
这里仅列举了部分应用场景,实际上 Redis 在各种领域中都有广泛的应用。其中的关键点在于对 Redis 的数据类型和功能的理解,充分发挥其特性。
Redis 为什么这么快?
Redis的高性能主要由以下几个因素决定:
- 内存存储:Redis 将所有数据保存在内存中,内存的读写速度远超过硬盘。当读取数据时,Redis 无需像磁盘数据库一样从硬盘读取,无需考虑磁盘I/O,磁头寻址等操作,大大减少了延迟。
- 非阻塞**I/O**:Redis 采用了基于多路复用技术的事件处理模型,可以处理大量并发请求。即使在高并发环境下,也能保持稳定的响应时间。
- 优化的数据结构:Redis 支持多种数据类型(比如字符串、列表、集合、散列、有序集),可以更精细地控制数据,使得对数据的操作更加具有针对性,进而提高操作效率。
- 单线程的操作设计:Redis 的操作都是单线程的,规避了传统数据库因为多线程引起的各类问题,比如锁竞争、上下文切换等(注:Redis 6 后引入了多线程模型)。
- 复制的特性:Redis 通过主从复制的方式实现数据的备份,并且复制操作是非阻塞的,除非处于同步复制(SYNC)的状态,否则主服务无需阻塞其他操作。
- 持久化设计:Redis 提供了两种数据持久化策略,RDB和AOF。用户可以根据自己的需求,选择合适的持久化策略,以平衡数据安全性和运行效率,这允许 Redis 在保持高性能的同时提供数据持久性。
总的来说,Redis的快速性能得益于其设计、数据结构、单线程模型以及内存存储等因素的相互结合。但是需要注意的是,虽然 Redis 的性能非常高,但由于其数据存放在内存中,因此在数据安全性和存储成本方面相对会有一些缺点,这需要根据具体需求来权衡。
Redis 有哪些优缺点
Redis 具有许多优点,但同时也存在一定的缺点。下面列举了 Redis 的优缺点:
优点:
- 性能高:Redis 将数据存储在内存中,读写速度快,适合缓存和实时数据处理。
- 多数据结构支持:Redis 支持多种数据结构,如字符串、列表、集合、散列、有序集等数据结构,可以满足各种应用场景的需求。
- 发布-订阅模式:Redis支持发布-订阅模式,可以应用于消息队列、实时通信等领域。
- 持久化:Redis支持 RDB 和 AOF 两种持久化方式,可以将内存中的数据定期保存到磁盘,保证数据的可靠性。
- 原子性:Redis 的命令是原子性的,可以保证在并发操作时数据的一致性。
- Lua脚本支持:Redis 支持 Lua 脚本,用户可以编写自定义脚本来实现复杂业务逻辑。
- 分布式锁支持:Redis 提供了简单的分布式锁实现,可解决多个系统实例间的资源竞争问题。
- 空间与时间效率:Redis 的数据结构设计兼顾空间和时间效率,可降低对系统资源的占用。
- 易于扩展:Redis 支持主从同步、Sentinel 集群管理和 Redis Cluster 分布式架构,可满足需要水平扩展的场景。
缺点:
- 内存限制:Redis 将数据保存在内存中,受限于物理内存大小,存储空间有限。
- 单线程处理:虽然避免了多线程引入的问题,但 Redis 的单线程处理也限制了它无法充分利用多核 CPU 资源。虽然对于大多数应用来说足够快,但在某些极端情况下可能导致性能瓶颈。
- 数据安全性:与磁盘存储的数据库相比,Redis 的数据存放在内存中,在某些情况下,比如突然断电,数据可能会丢失部分最近的更改。
- 高昂的存储成本:相较于磁盘存储的成本,内存存储在大容量和长时间存储场景下成本较高。
- 集群管理复杂:尽管 Redis 支持集群管理,但与其他分布式数据库相比,搭建和维护 Redis 集群的复杂性较高。
- 数据安全性: Redis默认情况下没有内置的身份验证机制,需要额外配置来确保数据安全。
Redis 线程模型
Redis 为什么要使用单线程?多线程性能不是更好吗?
在讨论这个问题之前,需要先看 Redis 的版本中两个重要的节点:
- Redis 4.0 引入多线程处理异步任务
- Redis 6.0 在网络模型中实现多线程 I/O
所以,网络上说的 Redis 是单线程,通常是指在 Redis 6.0 之前,其核心网络模型使用的是单线程。且 Redis6.0 引入多线程I/O,只是用来处理网络数据的读写和协议的解析,而执行命令依旧是单线程。
Redis 使用单线程模型的主要原因是为了简化设计和降低开发复杂度,并且在内存操作场景下,单线程已经足够支撑高性能表现。我们看官方给出的答案:

核心意思是:CPU 并不是制约 Redis 性能表现的瓶颈所在,更多情况下是受到内存大小和网络I/O的限制,所以 Redis 核心网络模型使用单线程并没有什么问题。而且使用单线程能够简化设计和降低开发复杂度。相比多线程使用单线程具有如下几个优势:
- 简化设计和开发:多线程编程会引入竞态条件、死锁等问题,相对复杂,难以调试。而单线程模型避免这些问题,使得 Redis 的架构更简单、直观且易于维护。此外,单线程模型消除了多线程环境下的同步、锁等成本。
- 充分利用**I/O**多路复用:Redis 使用 I/O 多路复用机制来同时处理大量客户端连接,即使在单线程模式下,它可以在相应时间内处理更多的请求。由于 Redis 主要是以内存为基础的数据库,因此绝大多数操作都是 CPU 绑定(CPU-bound)的,所以它的性能主要受这些操作本身的时间复杂度所限制。
- 原子性操作: Redis 支持原子性操作,这是单线程模型的自然优势之一。在 Redis 中,一些常见操作,如自增、自减、集合操作等,可以在单个命令中执行,而不需要多个命令之间的锁操作。
虽然多线程具有潜在的性能优势,例如在多核 CPU 系统中充分利用资源,但对于 Redis 而言,它的内存操作速度已经非常快,并且大部分情况下,I/O延迟和网络延迟才是瓶颈。因此,在内存存储数据库的场景下,单线程模型已经能达到很高的性能,而避免了多线程带来的复杂性和开发成本。
当然,在特定场景下,如需要对磁盘操作或计算密集型任务进行处理时,例如Redis的持久化、压缩等,可以选择使用多线程以获得更好的性能,但这并不妨碍其在内存操作时坚持使用单线程。
请详细介绍 Redis 的单线程模型?
在 Redis 中,服务器采用了单线程模型来处理客户端的请求。尽管只使用单个线程,利用I/O多路复用技术和高效内存操作,Redis 实现了高性能的数据存储和检索。以下是 Redis 单线程模型的详细解释,包括其工作流程:
- 客户端连接:当客户端发起连接请求时,Redis服务器接受这些连接,并创建一个客户端对象与文件描述符(File Descriptor)来表示该客户端的连接。
- 读取请求:当客户端向服务器发送命令请求时,Redis服务器使用单线程在非阻塞的方式下从多个客户端连接上读取数据。这里的非阻塞方式是基于I/O多路复用技术(如epoll、select、kqueue等)实现的。利用这个技术,服务器可以同时监控多个客户端连接,高效地读取输入的命令请求。
- 命令队列:Redis服务器将读取到的命令请求插入一个命令队列,以便逐个处理。为了保证线程安全和数据操作的顺序,每个命令请求都在此队列中等待服务器执行。这个队列也能确保命令按照FIFO(先进先出)的顺序执行。
- 命令处理:Redis服务器从命令队列中依次取出命令,并对数据执行相应的操作。由于在单线程环境中,服务器一次只处理一个命令,因此可以保证数据操作的原子性,不需要使用锁或其他同步机制。
- 响应返回:完成命令处理后,Redis服务器将结果返回给相应客户端。和读取请求类似,Redis在写入响应时仍然使用单线程非阻塞方式。当一个客户端连接准备好发送数据时,服务器将响应写入输出缓冲区,并使用多路复用技术将输出缓冲区的数据发送到对应的客户端。
Redis 6.0 后为何要引入多线程?
Redis 6.0引入了I/O-Threading模型,主要目的是进一步提高 Redis 的性能。
具体来说,Redis 6.0 的多线程主要用于处理网络I/O操作,而不是数据的读写操作。这是因为随着网络硬件的性能提升,Redis 的性能瓶颈往往在网络I/O,而不是数据的处理速度。所以为了提高网络 I/O 的并行度,Redis 6.0 对于网络 I/O 采用多线程来处理。
再具体一点,Redis 需要处理大量的客户端连接。对于每一个客户端,Redis 都需要接收请求、发送响应,而每一次的网络操作都需要系统调用,导致频繁的上下文切换,这会带来巨大的性能损耗。因此,通过使用多个线程并行地进行网络 I/O 操作,可以有效地减少上下文切换的数量,进一步提升处理网络请求的性能。
但是,Redis 在处理真正的命令时,仍然使用单线程模型。主要原因是多线程可能会引入竞争条件,使得实现和维护变得复杂。此外,由于 Redis 的数据处理大部分都是内存操作,这些操作已经非常快速,多线程对于这些操作的性能提升其实相对有限。
总的来说,通过引入多线程进行网络 I/O 操作,Redis 6.0在保持数据处理的简单性和高效性的同时,进一步优化了处理网络请求的性能。
Redis 6.0 多线程的实现机制是怎样的?
Redis 6.0 的多线程实现机制主要集中在网络 I/O 操作上。在这个版本中,Redis 引入了多个线程来处理客户端连接的读写操作,而在处理实际的数据命令时仍然保持单线程。
以下是Redis 6.0多线程实现的详细机制:
- 线程创建和管理Redis 6.0 在启动时,会根据配置文件或命令行参数创建一定数量的线程,这些线程被称为 I/O 线程,它们的主要任务是读取客户端的请求和发送响应。在运行期间,Redis 服务器对这些线程进行统一管理。
- 任务分发当客户端发送请求到 Redis 服务器时,主线程首先负责接收到来自客户端的数据,然后将请求分发给 I/O 线程去读取。一种简单的任务分发方式是采用轮询法(Round-Robin),即依次将请求分给不同的I/O线程。
- 线程安全的数据读写在 I/O 线程读取客户端请求数据时,可能会遇到并发操作的问题。为确保线程安全,Redis 6.0为每个客户端连接维护一个私有的读写缓冲区,以避免多个线程同时操作同一缓冲区造成的数据竞争。
- 动态命令处理一旦 I/O 线程读取完请求数据,主线程会继续处理这些请求。包括解析请求、执行相应的命令、以及将命令结果放回到客户端对应的输出缓冲区。需要注意的是,在处理实际的数据命令时,Redis 始终使用单线程模式。
- 异步响应客户端在准备好将数据发送回客户端之前,Redis 会再次使用 I/O 线程来完成这个操作。每个 I/O 线程将负责向其对应的客户端并行发送响应。
这些实现机制确保了 Redis 6.0 能够同时处理大量的客户端网络 I/O 操作,而不会牺牲数据处理的简单性和效率。需要注意的是,多线程功能需要在Redis 6.0配置文件或启动参数中开启(io-threads配置选项),默认情况下,Redis仍然使用单线程模式。
默认情况下 I/O 多线程只针对发送响应数据(write client socket),并不会以多线程的方式处理读请求(read client socket)。要想开启多线程处理客户端读请求,就需要把 Redis.conf 配置文件中的 io-threads-do-reads 配置项设为 yes:
io-threads-do-reads yes
同时, Redis.conf 配置文件中提供了 IO 多线程个数的配置项:
// io-threads N,表示启用 N-1 个 I/O 多线程(主线程也算一个 I/O 线程) io-threads 4
Redis 6.0开启多线程后,是否会存在线程并发安全问题
Redis 6.0 的多线程主要用于处理网络 I/O 操作,而非数据操作,因此数据操作依然是单线程进行,所以不会存在数据并发安全的问题。
为什么处理网络 I/O 时不会有线程安全问题呢?这是因为 Redis 对于这部分的处理有两个关键设计:
- 任务队列无锁设计:Redis会为每个I/O线程创建一个任务队列,主线程将任务添加到每个I/O线程的任务队列中。这个过程没有涉及到并发修改数据,因此可以避免加锁的开销。
- 线程间通信与读写分离:读写操作也被明确地划分给不同的线程处理,因此在处理同一客户端连接时,不存在同时读写的情况。每个客户端连接都有自己的读写缓冲区,可以确保在处理网络数据时不会出现数据竞争的问题。
因此,尽管 Redis 在 6.0 版本引入了多线程,但通过上述设计,Redis 仍然能确保操作的线程安全。这样既可以提高处理网络 I/O 的性能,又可以保证数据库操作的原子性和一致性。
Redis 数据类型
Redis 支持的数据类型有哪些?应用场景是什么?
Redis 支持以下五种主要的数据类型:
- 字符串(String)
- 哈希(Hashes)
- 列表(Lists)
- 集合(Sets)
- 有序集合(sorted set)
除了这些主要的数据类型外,Redis 还支持 bitmaps, hyperloglogs 和 地理空间、索引半径查询等数据类型。
一个字符串类型的值能存储最大容量是多少?
在Redis中,一个字符串类型的值最大能容纳的数据量为512MB。 也就是说,您可以将一个最大512MB的数据项存储在Redis的字符串值中。这个限制足够应付大部分使用场景,但是需要注意不要超出这个限制,否则会导致错误。
Redis 事务
了解 Redis 事务的概念吗?
Redis 事务的本质是通过 MULTI、EXEC、WATCH等一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。
总结说:Redis 事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。
Redis 事务分为哪几个阶段?
Redis 的事务分为以下四个阶段:
- 开始事务:使用 MULTI 命令来开始一个事务。
- 命令入队:在开启事务之后和执行事务之前,你输入的所有 Redis 命令都不会立即执行,而是加入到一个队列中。
- 执行事务:通过 EXEC 命令来执行队列中的所有命令,这也标志着事务的执行阶段。
- 事务结束:EXEC 命令执行后,整个事务就被认为是完成的。不做任何操作即可结束事务。
需要注意的是,在 Redis 中,事务完成后不支持回滚。你需要自己处理事务过程中可能出现的任何错误。直到 EXEC 命令被调用,所有的操作才会开始逐一执行,如果中间有命令执行失败,后续的操作则不会被终止。
Redis 事务支持ACID 吗?
Redis 的事务不完全符合传统的 ACID(原子性、一致性、隔离性以及持久性)事务模型。在Redis事务中,有以下特点:
- 原子性(Atomicity):Redis 的事务不完全满足原子性。Redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务中任意命令执行失败,其余的命令仍会被执行。
- 一致性(Consistency):Redis 的数据模型简单,并不保证类似于关系数据库的复杂一致性约束,但可以确保单个命令的原子性,以及通过 Lua 脚本可以实现复杂的原子操作,基于这种方式,你可以自己在业务层面保证一致性。
- 隔离性(Isolation):Redis 的事务以串行化的方式执行,确保在同一时刻只有一个事务在执行,所有的事务都是串行的,事务之间互不影响,所以它满足隔离性。
- 持久性(Durability):只要事务成功执行,更改就会被持久化到磁盘,Redis 可以通过 RDB 或 AOF 进行持久化。
但是你需要注意的是,Redis 在执行事务的时候并不支持回滚操作,在事务过程中的任何错误都需要你自己处理。也就是说,如果事务队列中某个命令执行失败,Redis 仍会继续执行下面的命令。因此,从这个层面上来看,Redis 事务并不完全满足 ACID 的全部条件。
Redis 数据过期删除
Redis 给缓存数据设置过期时间有什么作用?
为 Redis 缓存数据设置过期时间具有以下作用:
- 内存管理:当数据设置了过期时间后,一旦超过这个时间,Redis 将自动删除该数据。这有助于释放内存空间,特别是在内存有限的环境中非常有用。通过自动清除不再使用的旧数据,保持内存的使用率处于可控状态。
- 数据时效性:设置过期时间还有助于保证数据的时效性。有时候,我们希望在某个时间点后不再使用某些数据,例如登录会话信息、验证码等等。在这些场景下,为数据设置过期时间可以确保过期后的数据不再被使用。
- 缓存策略:设置过期时间可以作为缓存策略的一部分,有助于选择更适合应用程序需求的缓存策略。
Redis的过期数据的删除策略了解么?
Redis 使用两种策略来判断和处理过期数据:惰性删除(Lazy eviction)和周期性清理(Periodic cleanup)。
惰性删除
惰性删除的做法是不主动删除过期 key,而是当我们访问一个 key 时,Redis 会先检查这个 keyi是否设置了过期时间,如果设置了并且已经过期,Redis 就会立刻删除这个键,而不会返回任何数据。这种方法叫做惰性删除,因为只有在你尝试访问一个键时才会进行判断和删除。
- 优点
- 缺点
定期删除
定期删除的做法是定期在后台进行清理工作,检查并清除过期的键。这个过程分为两步进行:
- Redis 随机选择一些键;
- 如果这些键中的大部分键都已经过期,Redis 就会继续随机选择一些键进行检查。这样,通过周期性的后台任务和抽样,Redis 能够逐步清理掉过期的键,即使这些键并没有被尝试访问。
定期删除是一个循环的流程。那 Redis 为了保证定期删除不会出现循环过度,导致线程卡死现象,为此增加了定期删除循环流程的时间上限,默认不会超过 25ms。
- 优点
- 缺点:
惰性删除策略和定期删除策略都有各自的优点,所以 Redis 选择「惰性删除+定期删除」这两种策略配和使用,,以求在合理使用 CPU 时间和避免内存浪费之间取得平衡。
Redis 内存满了会发生什么?
在 Redis 的运行内存达到了某个阀值,就会触发内存淘汰机制,这个阀值就是我们设置的最大运行内存,此值在 Redis 的配置文件中可以找到,配置项为 maxmemory,如下:
//获取maxmemory配置参数的大小 127.0.0.1:6379> config get maxmemory //设置maxmemory参数为100mb 127.0.0.1:6379> config set maxmemory 100mb
Redis 内存淘汰策略有哪些?
Redis 提供了一些内存淘汰策略(Eviction Policy),以便在内存空间不足时处理新的写入请求。Redis 一共提供了 8 中内存淘汰策略。
noeviction
当内存不足以容纳新写入数据时,新写入操作会报错。这是默认策略。
- 使用场景:适用于那些需要确保每个键都不会被移除的数据持久化场景。
- 优点:所有键都是持久的,不会被自动删除。
- 缺点:如果内存不足以容纳新的写入请求,Redis 会返回一个错误:“OOM command not allowed when used memory > 'maxmemory'”。
allkeys-lru
根据 LRU 算法删除任何可能的 key。LRU,即最近最少使用策略,选择最近最少使用的数据进行淘汰。
- 使用场景:适用于 Redis 主要用作缓存的场景,无论键是否有设置过期时间,最少使用的键都有可能被淘汰以释放内存。
- 优点:相对公平地回收内存,释放出空间来容纳新的数据。
- 缺点:如果某些不常使用但重要的数据没有设置过期时间,也可能被回收。
volatile-lru
根据 LRU 算法但只删除设置了过期时间的 key。同样基于最近最少使用策略,但是只能淘汰那些设置了过期时间(即,有 ttl 信息)的 key。
- 使用场景:适用于 Redis 主要用作缓存,且键都设置了过期时间的场景。这种策略只会从设置了过期时间的键中选择最少使用的键进行回收。
- 优点:不会影响到没有设置过期时间的持久化数据。
- 缺点:如果没有过期时间的键占用较多内存,那么回收的空间可能不足以满足需求。
allkeys-random
随机删除所有 key。无规律的随机删除某些 key。
- 使用场景:适用于从所有键中随机选择进行驱逐的场景,实际使用较少。
- 优点:简单地随机删除键,它释放出空间,理论上不会产生热点。
- 缺点:可能会随机删除重要数据,不适合大多数场景。
volatile-random
随机删除过期设置了过期时间的 key。同样是随机删除某些 key,但是只能删除那些设置了过期时间的 key。
- 使用场景:适用于当 Redis 数据集中有大量带过期时间的键,且内存资源稀缺时。此策略只从设置了过期时间的键中随机移除某个键。
- 优点:不会影响到没有设置过期时间的持久化数据。
- 缺点:可能删除有用的缓存数据,这会导致缓存命中率下降。
volatile-ttl
只有设置了过期时间的 key 会被删除,且优先删除 TTL 值较小的 key,也就是说优先删除那些“即将过期”的 key。
- 使用场景:适用于具有过期时间的键的应用场景中,尤其是需要按键的剩余生存期优先回收的场合。
- 优点:回收那些剩余生存期较短的键,这些键本来也即将被删除,减小了删除有用数据的可能性。
- 缺点:如果大部分过期键的剩余生存期相差不大,那么回收策略的效果可能不明显。
allkeys-lfu
根据 LFU 算法删除任何可能的 key。LFU,即最不经常使用策略,选择最不经常使用的数据进行淘汰。
- 使用场景:策略适用于当你希望 Redis 实例中所有的 key 都被考虑进来,而淘汰策略根据各个 key 的使用频率(Frequency)来决定,使用这种策略,将会淘汰那些使用频率最低的 key。
- 优点:当你的数据分布广泛且访问频率不一致时,LFU 更能精确地反应一个 key 的热度。在一些长期运行的系统中,LFU 可以比 LRU 提供更好的缓存击中率。
- 缺点:在 Redis 数据集很大且访问差异性很大的情况下,可能会淘汰长时间未被访问,但仍有用的数据。此外,如果一个 key 刚刚添加进来但被频繁访问,那么这个 key 的 LFU 值会迅速增加,使得它不容易被淘汰。
volatile-lfu
根据 LFU 算法删除设置了过期时间的 key。基于最不经常使用策略,只能淘汰那些设置了过期时间的 key。
选择适当的内存淘汰策略需要根据实际的应用场景和需求来考虑。为了保持 Redis 的性能和数据质量,最好别让内存使用达到极限,通过监控工具定期查看 Redis 的内存使用情况,并对数据进行合理的规划和使用。
- 使用场景:适用于不会考虑全局的 key,只有那些被设置了 TTL 的 key。这个策略是为了避免一些重要的、未设置过期时间的 key 被错误地淘汰。
- 优点:可以在淘汰数据时,保护那些未设置过期时间的重要数据,这在一些场景下会更加有用,比如一部分重要数据永远不会过期。
- 缺点:可能会使得一些本应被淘汰的、频率不高但设置了过期时间的 key 得以保留,而一些可能更加重要但过期时间较快的key被淘汰,这就需要你仔细考量设置过期时间的策略。
如果要配置具体的内存淘汰策略,可以在redis.conf配置文件中配置,具体如下:
// 获取maxmemory-policy配置 127.0.0.1:6379> config get maxmemory-policy // 设置maxmemory-policy配置为allkeys-lru 127.0.0.1:6379> config set maxmemory-policy allkeys-lru
Redis 持久化
Redis 持久化有几种方式?
Redis 提供了两种持久化数据的方式,分别是 RDB(Redis DataBase)和 AOF(Append Only File)。
RDB****(Redis DataBase)
RDB** 方式按照设定的规则,定期将内存中的数据写入到硬盘的二进制文件中,也就是 snapshot 快照写入。**
规则可以自定义,例如每过 15 分钟,如果 Redis 里面至少有 1 个 key 发生变化,就自动保存。当 Redis 重启之后会通过读取 dump.rdb 文件恢复数据。
RDB 的优点在于:
- RDB 是一个非常紧凑的数据集的单文件表示形式,适合用于备份,全量复制等操作。
- Redis 重载一个 RDB 文件进行数据恢复是非常快的,特别适合用于灾难恢复。
缺点在于:
- 如果你需要尽可能避免数据丢失,那么 RDB 可能不适合你,因为最近的操作可能不会被持久化。
- RDB 进行保存操作时,父进程可能需要进行fork() 操作,当数据库很大时,这个操作可能会比较耗费服务器性能。
AOF(Append Only File)
AOF 持久化方式会记录每一次对服务器写的操作,当 Redis 重启的时候会重新执行这些命令来恢复原始的数据,AOF 文件的保存位置和 RDB 文件一样,都是在 Redis 配置文件中指定的。
AOF 的优点:
- 一般情况下 AOF 文件比同等数据下的 RDB 文件要大,但 AOF 的数据完整性要高很多,且人类可读。
- AOF 提供了更多的策略,比如每秒同步一次,每次写入都做同步,或者不做同步。
缺点在于:
- 对于相同的数据集,AOF 文件通常比 RDB 文件要大。
- 根据所使用的 fsync 策略,AOF 在某些配置下可能会比 RDB 慢一些。
更多的时候我们可能会选择同时开启两种持久化方式,让 AOF 作为数据恢复的主要手段,让 RDB 作为数据备份用途,因为 RDB 更适合用于保存历史数据。
RDB 快照是如何实现的呢?
RDB 快照实现的原理主要通过使用子进程(fork)完成数据的持久化。以下是 RDB 快照的主要实现步骤:
- 创建子进程当 Redis 需要生成一份 RDB 快照时,首先会调用 fork() 函数创建一个子进程。此时,子进程将会获得与父进程相同的内存地址空间副本。
- 子进程写入快照:子进程拥有与父进程一样的键值数据副本,因此子进程可以使用相应的数据结构把数据写入一个临时的磁盘文件,生成 RDB 快照(一般是名为dump.rdb的二进制文件)。
- COW(Copy-On-Write,写时复制):当子进程在持久化过程中,若父进程有新的写操作,为了不影响到子进程正在操作的内存,操作系统采用 COW 技术避免了数据的同步问题。在父进程试图修改某个数据页时,操作系统会先复制一份数据页,并让父进程操作这份复制页。这样一来,子进程可以继续使用原始数据页进行持久化操作,而父进程可以对自己的复制页进行修改,两者相互不干扰。
- 替换旧快照并删除子进程:当子进程完成快照写入后,父进程会将这个新产生的 RDB 文件替换成原先的快照文件,而后子进程会被销毁。此过程父进程可以继续处理客户端命令请求,与子进程的持久化操作互不影响。
需要注意的是,fork() 操作在 Redis 数据集较大时可能会造成较大的内存和 CPU 消耗。但是,由于使用了 COW 机制,在大部分情况下,实际额外内存的使用仅是写入操作的数量,而且操作系统会对 COW 进行优化,实际影响并不会特别大。
总的来说,RDB 持久化通过使用 fork 创建子进程、子进程持久化数据、COW 内存管理技术以及文件的替换等一系列操作来实现数据快照持久化。这种实现方式在大多数场景下可以在一定程度上保证数据的持久化与备份,同时不影响 Redis 主进程对于客户端的请求处理。
AOF 日志是如何实现的?
AOF 日志通过记录并保存 Redis 服务器已经执行过的所有写入操作命令来实现数据的持久化。具体过程如下:
- 记录操作:当执行一个写入命令(例如SET)时,Redis 不仅会将这个命令执行并修改内存中的数据,还会将这个命令追加到 AOF 文件的末尾。
- 文件同步:配置文件中的 appendfsync 选项控制着 Redis 何时将AOF缓冲区的内容同步到硬盘。Redis提供了三种同步方式:
- 恢复数据:当 Redis 重启启动时,它会通过重新执行 AOF 文件中保存的所有写入命令来恢复数据。
- 日志重写:AOF日志文件会随着写入命令的增多不断变大。当 AOF 文件大小超过一定程度时,Redis会启动 AOF 重写机制。重写时,Redis 会创建一个新的 AOF 文件,新文件中的内容是当前Redis数据库中所有键值对的最小写入命令集合。重写过程可以在后台进行,不影响正常的读写操作。重写 AOF文件的过程称为 bgrewriteaof,它使用的机制与 RDB 的 bgsave 类似:先执行一个 fork 操作创建子进程,然后由子进程来执行整个重写操作,最后利用 rename 原子操作来替换旧的 AOF 文件。
总的来说,AOF 日志是通过保存和重播写入命令来实现的,这种方法相比于 RDB 的快照方式有着更好的数据持久性保证。同时,通过设定不同的 fsync 策略和开启 bgrewriteaof 机制,AOF 也能在一定程度上保证写入性能以及控制 AOF 文件大小。
为什么会有混合持久化?
混合持久化是 Redis 同时启用 RDB 快照和 AOF 日志两种持久化方式。这样做的原因主要是基于以下两方面的考虑:
数据安全性和可靠性
尽管 RDB 和 AOF 各自都有一定程度的数据安全性和可靠性保障,但它们也各自具有一些限制。
- RDB 以快照的形式定期持久化数据,但在两次快照之间会存在数据丢失的风险。
- AOF 通过实时地记录写入命令来持久化数据,具有较高的数据可靠性,但在某些情况下重播 AOF 日志可能需要较长的时间(尤其是 AOF 日志文件较大时)。
混合持久化结合了 RDB 和 AOF 的优势,既能保证数据的高可靠性,也能尽量减少数据丢失的风险。当 Redis 需要恢复数据时,首先尝试加载 RDB 文件,恢复的数据可能会落后于实际最新的数据;然后,再通过 AOF 日志重播最近的写入命令,以实现数据的恢复。这样,在发生故障或需要重启时,数据丢失的可能性会大大降低。
性能和资源利用
- RDB 持久化过程中可能会产生较大的内存消耗,特别是在大型数据集情况下。
- AOF 持久化会产生更多的 I/O 开销,尤其是在 appendfsync 配置为 always 时。
通过混合持久化,可以在一定程度上平衡这些资源消耗,灵活地配置持久化策略。例如,可以设置 RDB 快照的保存时间间隔,限制快照产生的频率,从而减小内存消耗。对 AOF 的fsync策略进行调整,降低 I/O 开销,提高写入性能。
综合以上考虑,混合持久化提供了一种更加灵活、安全且可靠的数据持久化方式。当同时需要高数据可靠性和较好性能时,混合持久化是一种很好的选择。
在生成 RDB 期间,Redis 可以同时处理写请求么?
在 Redis 生成 RDB 期间,可以通过使用 bgsave 命令在后台执行快照操作,从而实现同时处理写请求。这主要利用了操作系统的 fork() 操作。
当执行 bgsave 命令时,Redis 使用 fork() 创建一个子进程。这个子进程会生成数据快照并将其保存到磁盘上的 RDB 文件中。由于 fork() 调用会将父进程(主 Redis 进程)的进程空间内存复制一份到子进程,因此在复制过程执行期间,父进程会继续处理客户端的读写请求。
需要注意的是,fork()操作可能会产生性能损耗,特别是在数据集较大的情况下。性能损耗主要来自两个方面:
- fork() 执行过程中需要复制内存,这会消耗一定的CPU和内存资源。
- 在执行写请求时,如果 Redis 修改了某些数据页,操作系统会按照 "写时复制"(Copy-On-Write, COW)策略为被修改的数据页创建一个新副本。这会导致额外的内存消耗和部分性能损耗。
总之,尽管在生成 RDB 过程中,Redis 可以通过使用 bgsave 命令在后台执行快照,从而实现同时处理写请求。但在实际运行中,可能会遇到一定程度的性能损耗和资源消耗。
Redis 分区
Redis 支持分区吗?怎么做?为什么要做?
Redis 支持分区(Partitioning)。分区是将数据分散到多个 Redis 实例上,以便在集群中使用数据。
选择分区主要考虑到分区有如下几个优点:
- 更高的吞吐量和性能: 当数据被分散到多个 Redis 实例上时,可以提供更高的处理吞吐量和性能,因为处理能力由多台服务器共同承担。
- 更好的数据管理和扩展性: 分区有助于管理较大的数据集,因为数据分布在多个节点上。这使得可以按需添加或删除存储容量,提高系统的扩展性。
- 高可用性: 如果将数据复制到各个分区节点的多个副本上,可以提高系统的可用性。当某个节点发生故障时,可以使用其他副本继续提供服务。
Redis 支持以下几种分区策略:
- 范围分区(Range Partitioning): 将数据分区为具有连续键范围的区间。例如,所有用户ID为1到1000的用户数据保存在实例A上,ID为1001到2000的用户数据保存在实例B上。这种方法的缺点是,起始和结束键范围的节点可能会承担更高的负载。
- 哈希分区(Hash Partitioning): 这种方法通过对键值进行哈希运算,然后根据哈希结果将数据放入相应的分区。它有助于实现数据分布的均衡,但是可能会导致跨分区操作的复杂性增加。
- 列表分区(List Partitioning): 类似于范围分区,但使用列表中的值而不是范围来划分键空间。例如,可以将所有管理员用户的数据存储在一个实例上,而将其他用户数据存储在另一个实例上。
- 一致性哈希(Consistent Hashing): 该方法克服了传统哈希分区中节点调整时的数据迁移问题。一致性哈希通过在一致性哈希环上放置节点,并将数据存储在离它最近的节点上实现。当添加或删除节点时,只需对相邻节点的数据进行调整。
Redis分区有什么缺点?
Redis 分区有如下几个缺点:
- 操作复杂性增加: 部分 Redis 的特性在使用分区后变得非常复杂或难以实现。比如涉及多个键的操作通常并不支持跨节点处理,例如,事务,管道和一些特定的键组操作。
- 数据重新分布困难: 添加或移除节点时,需要进行数据重新分布。这个过程可能需要复杂的计算和操作,而且在数据迁移期间可能会对性能产生较大影响。
- 增大了故障处理复杂性: 如果使用多节点的分区策略,每个节点都可能出现故障,这将增加故障处理和数据恢复的复杂性。
- 一致性问题: 分区可能导致数据一致性问题。例如,在写入数据后,需要时间才能将数据同步到所有的副本中,这样就可能出现在一段时间内读取不到最新数据的问题。
- 不支持复杂查询: 分区后,不支持跨节点的复杂查询,或者查询成本非常高。
Redis 主从复制
了解主从复制的原理吗?
Redis 的主从复制是一种在多个Redis服务器节点间实现数据同步的策略。通过将一台或多台从(Slave)服务器配置为复制一个主(Master)服务器的数据,以实现数据冗余、读操作负载均衡,提高系统的可用性和容错能力。其核心原理:
- 建立连接: 当 Slave 启动时,它会通过配置文件中指定的 Master 的地址和端口建立一个同步连接。如果没有配置文件,也可以手动使用 SLAVEOF 命令来指定主服务器。
- 初始化同步: 当 Slave 成功连接到主服务器后,它会发送一个 PSYNC 命令给 Master。这里分为两种情况
- BGSAVE 和 RDB 文件交换: 当收到全量同步请求后,Master 会执行 BGSAVE 命令创建一个 RDB 格式的数据快照。然后,它将生成的RDB文件通过同步连接发送给 Slave。Slave 在接收到 RDB 文件后,会将其保存到磁盘并载入到内存;这个过程中,Slave 会中断处理客户端命令直至数据加载完成。
- 主服务端的命令缓冲区: Master 会将执行过程中产生的所有写操作命令以及与数据同步有关的命令(如 EXPIRE)记录到一个命令缓冲区,即 Replication Buffer。这个缓冲区可以保证 Slaver 能得到最新的数据更新。
- 持续复制增量更新: Slave 在完成 RDB 文件加载后,会继续接收并应用主服务器的数据更新,即不断从 Master 同步增量数据。Master 会将缓冲区中的命令发送给 Slave,Slave 在解析命令后执行,以保持与 Master 数据的一致性。
- 心跳检测和故障处理: Master 与 Slave 之间会定期发送心跳包和偏移量确认,确保连接正常与维护数据同步状态。当Slave 和Master 之间的连接出现问题时,Slave 会自动尝试重新进行连接和同步。同时,在Master 故障时,可以通过手动或自动故障转移(如 Sentinel 机制)将一个 Slave 提升为新的主服务器。
由于主从延迟导致读取到过期数据怎么处理?
- 通过scan命令扫库:当Redis中的key被scan的时候,相当于访问了该key,同样也会做过期检测,充分发挥Redis惰性删除的策略。这个方法能大大降低了脏数据读取的概率,但缺点也比较明显,会造成一定的数据库压力,否则影响线上业务的效率。
- 动态刷新过期键。它改善了 Redis 的键过期策略,以处理主从复制时因网络延迟导致从服务器读取到过期数据的问题。在早期版本的 Redis 中,每隔一段时间只能清理一部分过期键,这可能导致一些键实际上已经过期,但是因为尚未被主服务器清理,所以从服务器仍然可以读取到这些键的值。而在 Redis 4.0 之后,Redis 引入了动态调度机制,能根据已过期但仍未被删除的键的数量,自动调整清理过期键的频率,从而更快地清理过期键。这个特性可以避免大量过期键堆积,在主从服务器复制时减少网络延迟导致的从服务器读取到过期数据的情况。需要注意的是,尽管这个新特性改进了过期键的清理,但是在网络延迟的情况下,从服务器仍然可能会暂时性地读取到过期的数据,这是分布式系统中数据一致性问题的一种常见表现。解决这个问题可能需要更强的一致性模型,或者应用程序需要有适当的策略处理这种情况。
主从复制的过程中如果因为网络原因停止复制了会怎么样?
如果在Redis的主从复制过程中因为网络原因导致复制中断,可能会有如下几个影响:
- 数据不一致: 主库和从库之间的数据会出现不一致。主库更新的数据不会被同步到从库中,导致从库数据和主库数据差异增大。
- 影响读性能: 如果你的系统设计是主库写、从库读,复制中断后,由于从库的数据不能及时更新,会影响读操作的准确性。
- 数据丢失风险: 如果主库出现问题需要恢复数据,此时从库如果没有最新的数据,将无法完成数据恢复的过程,有数据丢失的风险。
如果出现网络故障断开连接了,Redis 会自动重连的,从2.8 版本开始,Redis就支持主从复制的断点续传。具体过程如下:
- 当网络恢复后,Slave会尝试重新连接Master并进行复制。如果复制过程中断的时间非常短,Slave可能会执行部分复制,只复制中断后Master上发生改变的那部分数据。如果Slave认为数据一致性问题较严重,可能会选择进行全量复制。
- 全量复制意味着从Master复制所有数据到Slave,但是这可能会对网络和系统资源造成较大压力,复制大量数据可能还会对Master的性能造成影响,甚至对正常服务造成影响。
Redis主从架构数据会丢失吗,为什么?
Redis主从架构数据可能会丢失,主要由以下几种原因导致:
- 网络问题:如果在数据复制过程中,主从服务器之间的网络连接中断,这可能导致从库无法获取到主库最新的数据,如果此时主库数据丢失,那么这部分数据则无法从从库恢复。
- 主服务器宕机:主服务器如果突然宕机,正在处理中的数据可能不会被复制到从服务器中,导致数据丢失。
- 复制延迟:复制操作不是实时的,存在一定的延迟。如果主服务器在这个延迟期内失败,那么从服务器可能还没来得及复制这部分最新数据,导致数据丢失。
- 磁盘故障:任何服务器(无论主服务器还是从服务器)磁盘故障都可能导致数据丢失。
- 不正确关闭:如果Redis没有正确关闭(如突然断电),可能会丢失还在内存中未持久化到磁盘的数据。
为了防止或者降低数据丢失的风险,可以采用以下措施:
- 使用持久化配置(如RDB或AOF)可以将内存中的数据保存到磁盘,提高数据安全性。
- 使用Redis的哨兵模式(Sentinel)或者集群模式,提供高可用支持,自动完成主服务器的故障转移。
- 部署多个从服务器,可以提高数据的可用性和冗余度,进一步减少数据丢失的风险。
- 对硬件、网络等设施进行冗余部署,提高系统稳定性。
Redis 哨兵机制
Redis哨兵是怎么工作的?
Redis哨兵是一种能够在运行时自动进行故障发现和恢复的系统。其主要作用是监控Redis实例(主服务器或副本),并在主服务器出现故障时,自动选择一个副本进行升级成为新的主服务器。
Redis哨兵的工作方式如下:
- 监控:哨兵常规会检测所有Redis实例,确认他们的运行状态和持续可用性。
- 通知:当被监测的某个Redis实例出现故障时,哨兵可以通过API向管理员发送通知。
- 自动故障切换:如果主服务器出现故障,哨兵将开始启动故障切换过程,自动选出一个副本来替代该主服务器。
- 配置提供者:客户端需要连接到Redis的正确主服务器,哨兵通过提供一份全面且最新的配置信息,帮助客户端在主服务器(master)出现故障时,找到新晋升的主服务器。
在选举新的主服务器(master)时,Redis哨兵遵循以下几个步骤:
- 哨兵会确认原先的主服务器发生故障。
- 然后,在所有哨兵之间进行投票选举一个哨兵作为领导哨兵。领导哨兵将负责处理故障切换的过程。
- 选择一个副本接任主服务器。哨兵会考虑每个副本的复制偏移量和运行id,选择最适合的副本作为新主服务器。
- 副本升级为主服务器后,其他所有的副本会自动连接到新的主服务器。
Redis 哨兵的故障转移是怎么样的过程?
Redis Sentinel的故障转移过程分为如下:
- 故障检测:每个哨兵定期检查它们监视的 Redis 实例(包括 Master和 Slave)的运行状态。如果 Master 没能在指定的时间内正确回应哨兵的 PING 命令,哨兵会将该 Master 标记为主观下线(SDOWN)。如果一个哨兵将Master标记为主观下线,那么它会向其它哨兵节点询问这个Master的状态,看它们是否也认为该Master下线。
- 故障确认:如果超过配置的数量(例如,超过半数)的哨兵节点都认为某个 Master 主观下线,那么这个 Master 会被标记为客观下线(ODOWN)。当一个 Master 被标记为客观下线时,开始执行故障转移的操作。
- 选举新的领导哨兵:哨兵群集将选出一个领导哨兵(领头哨兵或者领主)来进行下一步的故障转移操作。通过Raft一致性算法选出新的守护哨兵,以控制故障恢复的整个过程。
- 选举新主服务器:领导哨兵会从所有的 Slave 中选出一个作为新的 Master。选择的规则主要包括:
- 发送 SLAVEOF 命令:领导哨兵向被选中的 Slave发送 SLAVEOF NO ONE 命令,将该 Slave 升级为Master。同时,向其他 Slave 发送 SLAVEOF <new-master> 命令,让它们成为新 Master的 Slave。
- 更新配置:所有的哨兵接收到新 Master 的信息后,将更新它们的配置。然后在心跳信息中传播新的Master信息,所有的哨兵都知道新的Master是哪一个。
- 故障恢复:新Master被选出后,旧的Master如果恢复了,那么在重新上线时它将成为新Master的Slave。
在整个故障转移过程中,Redis的哨兵模式还要处理一些特殊情况,比如说网络分区,也就是在选举新主服务器的时候有可能出现一半的哨兵节点看不到另一半哨兵节点的情况。在这种情况下,哨兵模式要保证只有一个主服务器被选出来,防止脑裂问题的发生。虽然一些执行细节可能因具体情况而略有不同,但这些步骤大致描述了哨兵的故障转移过程。
故障转移时会从剩下的slave选举一个新的master,被选举为master的标准是什么?
如果一个master被认为odown了,而且majority哨兵都允许了主备切换,那么某个哨兵就会执行主备切换操作,此时首先要选举一个slave来,会考虑slave的一些信息。
1、跟master断开连接的时长。
如果一个slave跟master断开连接已经超过了down-after-milliseconds的10倍,外加master宕机的时长,那么slave就被认为不适合选举为master。
( down-after-milliseconds * 10) + milliseconds_since_master_is_in_SDOWN_state
2、slave优先级。
按照slave优先级进行排序,slave priority越低,优先级就越高
3、复制offset。
如果slave priority相同,那么看replica offset,哪个slave复制了越多的数据,offset越靠后,优先级就越高
4、run id
如果上面两个条件都相同,那么选择一个run id比较小的那个slave。
同步配置的时候其他哨兵根据什么更新自己的配置呢?
执行切换的那个哨兵,会从要切换到的新master(salve->master)那里得到一个configuration epoch,这就是一个version号,每次切换的version号都必须是唯一的。
如果第一个选举出的哨兵切换失败了,那么其他哨兵,会等待failover-timeout时间,然后接替继续执行切换,此时会重新获取一个新的configuration epoch 作为新的version号。
这个version号就很重要了,因为各种消息都是通过一个channel去发布和监听的,所以一个哨兵完成一次新的切换之后,新的master配置是跟着新的version号的,其他的哨兵都是根据版本号的大小来更新自己的master配置的。
为什么Redis哨兵集群只有2个节点无法正常工作?
Redis的哨兵模式是采用一种分布式的一致性协议,比如说Raft或者Paxos来确保在选举新的主库(Master)时系统的一致性。这意味着,当网络分区、停机或者其他故障发生时,仍能保证最终的一致性。
在这样的协议中,一个常见的规则就是集群中节点的数量必须大于一半才有权力进行主节点的选举。这是为了防止"脑裂"情况的发生,即有两个节点都认为自己是主节点,独立进行操作,最后导致数据不一致。
因此,对于只有两个节点的哨兵集群,当其中一个节点挂掉或者网络问题导致两者无法通信时,剩下的节点并不能确认它自己是否还是主节点,因为在这种情况下,无法判断有多于一半的节点认可它为主节点(在这个例子中,"多于一半"的节点数就是2,但是只剩下一个节点了),所以就无法进行正常工作。
这就是为什么在配置哨兵模式的时候,通常建议配置奇数个且至少为3个哨兵,这样就可以在有节点挂掉或者通信问题的情况下,仍然有多于一半的节点可以进行主节点的选举,保证系统的正常运行。
Redis Cluster
Redis Cluster中是如何实现数据分布的?这种方式有什么优点?
在 Redis Cluster 中,数据分布是通过哈希槽的机制实现的。
数据分布的实现
- 哈希槽:Redis Cluster 为了实现数据的水平划分,引入了一个叫做哈希槽的概念。共有 16384 个哈希槽。当需要保存或检索一个键时,Redis 会计算这个键对应的哈希槽。
- 键的映射:键被映射到哈希槽上是基于它的 CRC16 值。具体计算如下:HASH_SLOT = CRC16(key) mod 16384
- 分配给节点:这些哈希槽会被分配到 Redis Cluster 中的各个主节点上。如果你有 N 个节点,那么每个节点大约会拥有 16384/N 个哈希槽。
- 存储数据:当一个键值对需要被保存时,Redis Cluster 会基于这个键计算哈希槽,然后找到这个哈希槽分配给的节点,并将数据存储到那个节点上。
它具有如下优点
- 线性扩展性:通过增加或减少节点,您可以很容易地扩展或收缩 Redis Cluster 的容量。当添加或删除节点时,哈希槽可以重新分配,从而实现集群的伸缩。
- 高可用性与容错:在 Redis Cluster 中,每个主节点都可以有多个副本节点。如果主节点失败,一个副本节点可以被提升为新的主节点,保证数据的可用性。哈希槽的设计确保只有一个小部分的数据会受到影响。
- 负载均衡:由于数据被分布在所有的节点上,负载也被均匀地分配到各个节点上,这避免了单个节点的热点问题。
- 透明的客户端重定向:如果客户端尝试从一个不正确的节点检索数据(即该节点不拥有数据的哈希槽),它会收到一个 MOVED 错误,并得知正确的节点地址。这使得客户端可以轻松地重定向请求到正确的节点。
- 低延迟操作:因为 Redis Cluster 的设计是去中心化的,大多数操作只需要与一个节点交互,而不需要多次跳转或中间协调,从而保证了低延迟。
Redis Cluster节点间通信是什么机制?
Redis Cluster 节点间的通信基于一个叫做 Gossip 协议的机制。Gossip 协议是一个轻量级、自组织、点对点的通讯协议,允许节点之间交换信息和发现集群中的其他节点。
下面是具体方式:
- 节点发现:
- 集群状态和配置传播:
- 故障检测和通知:
Redis Hash 冲突怎么解决?
Redis Hash 冲突是多个键被映射到同一个哈希槽中而引起的冲突,解决方案有如下几种:
开放地址法
当哈希冲突发生时,Redis 不会在该槽中创建一个链表(与许多其他哈希表实现不同),而是会查找下一个可用的槽位。具体步骤如下:
- 计算键的哈希值,并找到对应的哈希槽。
- 如果该槽已经被占用(哈希冲突),则向后移动到下一个槽。
- 继续这个过程,直到找到一个空槽或已经包含该键的槽。
如果循环回到初始的槽,这意味着哈希表已满,此时需要进行哈希表的扩容。
哈希表的扩容与收缩
为了使哈希表保持在一个合适的大小,Redis 会根据其负载因子(即已用的槽位与总槽位的比例)来决定是否需要扩容或收缩。
- 扩容:当负载因子超过 1 或者已用的槽位超过哈希表大小的一半时,Redis 会选择扩容哈希表。
- 收缩:当负载因子小于 0.1 时,Redis 会考虑收缩哈希表,释放内存。
渐进式 rehash
为了避免在扩容哈希表时造成长时间的阻塞,Redis 使用了一种称为“渐进式 rehash”的策略。在这个过程中,Redis 会逐步地将旧哈希表中的键移到新的哈希表中。在 rehash 过程中,Redis 会同时维护两个哈希表,并确保所有的读写操作都是正确的。
通过开放地址法来解决冲突,配合哈希表的动态扩容与收缩,以及渐进式的 rehash 策略,Redis 能够高效地管理其内部的哈希表,并解决哈希冲突问题。
哈希槽又是如何映射到 Redis 实例上呢?
在 Redis Cluster 中,为了分布数据和负载均衡,所有的数据被分成了 16384 个哈希槽。Redis 是通过如下几个步骤将哈希槽映射到 Redis Cluster 中的各个节点上
1、哈希槽的计算
对于给定的键,Redis 使用以下公式来计算其对应的哈希槽:
HASH_SLOT = CRC16(key) mod 16384
其中,CRC16(key) 是键的 CRC16 值。这会产生一个介于 0 和 16383 之间的数,代表哈希槽的编号。
2、哈希槽的分配
当我们设置 Redis Cluster 时,16384 个哈希槽会被分配给所有的主节点。例如,如果有 3 个主节点,那么可能是这样分配:
- Node A: 哈希槽 0 - 5460
- Node B: 哈希槽 5461 - 10922
- Node C: 哈希槽 10923 - 16383
这只是一个例子。在实际的配置中,哈希槽的分配可以是不均匀的,取决于您的需求和配置。
3、键的存储和检索
当一个键需要被存储或检索时,Redis Cluster 首先会计算该键的哈希槽。然后,它会查找哪个节点负责该哈希槽,并将请求路由到那个节点。
4、哈希槽的迁移
为了实现集群的伸缩性和容错能力,Redis Cluster 允许哈希槽在节点之间进行迁移。这意味着,如果某个节点变得过于繁忙或出现故障,您可以重新分配其哈希槽到其他节点,从而实现负载均衡和容错。
5、与客户端的交互
如果客户端尝试访问一个键,而这个键所在的哈希槽并不在当前连接的节点上,那么该节点会返回一个 MOVED 错误,告诉客户端正确的节点地址。大多数现代的 Redis 客户端都能够处理这种情况,并自动重定向请求到正确的节点。
Redis 高可用
介绍下Redis单机模式
Redis单机模式是指在单个服务器上运行的Redis实例,它仅包含一个Redis进程,同时负责处理读写操作。在这种模式下,数据存储在单个服务器上,并对外提供服务。它的配置和部署相对简单,适用于小型应用或者快速原型开发。
使用场景:
- 小型应用:对于数据量较小且访问量不是特别高的应用,单机模式足以满足需求。
- 开发与测试环境:在开发和测试阶段,为了简化环境和方便开发,通常会使用单机模式。
- 非关键数据缓存:适用于缓存非关键数据,对数据持久性和高可用性要求不高的场景。
优点:
- 简单易用:Redis单机模式的配置和部署相对方便,需要的资源较少,操作方便。
- 性能较高:Redis是内存数据库,基于单个服务器的性能表现较好,适用于需要快速响应的场景。
- 易于开发与维护:单机模式的开发和维护成本较低,便于开发者快速开发和调试应用。
缺点:
- 可扩展性差:由于数据存储在单个服务器上,当数据量增长到一定程度或者访问量逐渐提高时,单机模式就无法满足需求了。
- 容灾能力有限:Redis单机模式下存储的所有数据仅存在于一个服务器上,如果发生硬件故障或其他问题,可能导致数据丢失。
- 不支持高可用:单机模式不具备主从复制或分区功能,如果服务器宕机,服务会中断,影响正常数据访问。
总结一下,Redis单机模式适用于小型项目、原型开发与测试环境等场景。它简单易用,性能较高,但缺乏可扩展性、容灾能力和高可用性。在实际应用中,如果数据规模和访问流量较大,需要考虑使用主从复制、分区或者集群等模式来满足需求。
介绍下 Redis 多副本模式
Redis多副本,采用主从(replication)部署结构,它主要由一个主节点和多个从节点组成,在这种模式中,主节点负责处理所有的写操作,从节点则用于复制主节点的数据。与单副本模式不同的是,多副本模式中,每一个主节点可以有多个从节点,以此来提供更高级别的数据冗余与可用性。
使用场景:
- 数据库冗余备份:多副本模式可以提供多个数据副本,当主节点出现故障时,可以立即从备份节点中恢复数据。
- 高读并发场景:通过读写分离,可以把大量的读操作负载到多个从节点上,从而提高系统的承载能力。
- 增强数据安全性:增加冗余度,减小数据丢失的可能性。如果某个从节点的数据出现问题,还有其它从节点可以提供服务。
优点:
- 高可用性:多副本模式通过多个从节点备份数据,当主节点出现故障时,可以立即从备份节点中恢复,从而提供高可用性。
- 读性能优化:通过读写分离,读请求可以分摊到各个从节点上,从而提升读性能。
- 数据安全性:通过复制数据到多个从节点,即使某个节点故障,也不会造成数据丢失,大大增强了数据的安全性。
缺点:
- 写性能瓶颈:所有的写操作都由主节点处理,如果数据的写入操作特别频繁,那么主节点就会成为性能瓶颈。
- 数据一致性:因为复制操作是异步进行的,所以可能会存在一定延时,这就造成了主从节点间的数据不一致的情况,虽然这个时间通常很短,但仍需要注意。
- 资源成本:对于每一个附加的副本,都需要额外的存储和网络带宽。对于资源有限的环境,可能需要更慎重地考虑是否使用多副本模式。
总结一下,多副本模式适用于需要较高数据冗余与可用性,同时具有较大读并发的应用场景,但是它对资源的需求较高,且可能存在数据一致性问题。在实际使用中,需要根据具体需求进行选择。
介绍下Redis Sentinel(哨兵)
Redis Sentinel是Redis官方推荐的高可用解决方案,主要负责监控Redis主服务和从服务运行状态,以及实现在主服务异常的情况下自动进行故障转移。
优点:
- 高可用性:Redis Sentinel能自动监控和识别主节点和从节点的状态,主节点宕机后能自动将从节点提升为主节点。
- 故障恢复:Redis Sentinel在主节点故障后可以自动进行故障转移,并提供服务。
- 集群监控:可以通过Sentinel持续监测Redis的运行状态,及时发现Redis的异常情况。
缺点:
- Sentinel自身的高可用:尽管Sentinel提供了检测Redis节点运行状态和故障转移的能力,但Sentinel自身可能也会出现单点故障,因此,实际应用中,我们通常会配置多个Sentinel以提供高可用。
- 数据强一致性:Sentinel无法解决Redis的数据强一致性问题,因为Redis的数据同步是异步的,主从切换期间可能会有数据丢失。
- 网络分裂:在网络极端环境下,可能会发生脑裂(网络分裂),即主节点和从节点无法通信但仍然对外提供服务,这会导致数据一致性问题。
Redis Sentinel能实现Redis高可用的监控,自动故障切换和服务通知,在需要高可用和故障自动恢复的场景中比较适用,但其自身可能出现的单点故障问题需要通过额外配置多个Sentinel来规避,缺乏强数据一致性,并且在网络极端环境下可能会出现网络分裂问题。
介绍下Redis Cluster
Redis Cluster是Redis官方推出的分布式解决方案,它包括多个Redis节点,这些节点通过哈希槽将数据分布在多个Redis服务上。它的主要功能是提供数据分片和容错。
使用场景
- 大数据量或高并发场景:由于Redis Cluster能够做到线性扩展,因此在传统单节点Redis无法应对的大数据量或高并发场景,可以使用Redis Cluster。
- 高可用需要:Redis Cluster可以自动将主节点的数据复制到从节点,当主节点不可用时,可以普通自动故障转移,进而提供高可用性。
- 数据分片需要:如果需要将数据存储在多个Redis实例中,以解决单个Redis实例内存不足的问题,可以使用Redis Cluster进行数据分片。
优点:
- 高可用:Redis Cluster可自动迁移和故障转移。当节点失效或者运行不正常时,Redis Cluster会自动进行故障转移,选择一个从节点晋升为新的主节点,实现服务的无缝切换,确保服务高可用。
- 数据分片:Redis Cluster通过哈希槽的方式将数据分布在各个节点上,不仅可以有效利用多个节点的内存,减轻单个redis实例内存压力,还能提升系统的读写能力。
- 扩展性好:当需要更大的存储或计算能力时,可以简单地添加更多的节点到集群中,实现线性扩展。
缺点:
- 数据强一致性问题:由于主从复制以及数据复制都是采用异步的方式,所以不保证强一致性,可能会在小概率情况下出现数据丢失的情况。
- 管理复杂:相比于单个Redis节点,管理一个分布式的Redis Cluster会更复杂,需要考虑节点的增加、减去、失效等情况。
- 支持有限的命令:部分redis命令在集群下不能使用,或者用法有所不同。
- 存在槽迁移时间:当进行槽迁移(数据重组)时,需要一定的时间进行数据转移。
Redis Cluster提供了一个数据分区、自动故障转移和线性扩展的分布式系统,并在一定程度上提高了存储容量和处理能力,适用于大数据量和高并发的场景。但与此同时,由于配置和管理的复杂性,可能增加系统的维护难度。同时,不保证强一致性,部分Redis命令不可用,以及在数据重组时存在迁移时间也是其主要不足。
Redis 高可用方案具体怎么实施?
Redis高可用方案的主要目标是保证Redis服务在主节点出现故障时能够自动切换到从节点,以保证服务不中断。以下是一些常见的高可用方案:
- 主从复制+Sentinel方案
- 主从复制+客户端
- Redis Cluster
- 第三方解决方案例如:Proxy、Twemproxy、Codis(主要针对国内)、HAProxy等。这些方案都需要自行部署和维护,要根据自身需求及团队经验进行选择。
目前,官方推荐使用Redis Sentinel和Redis Cluster方案,这两种方案在大多数场景下能满足高可用需求。在实际场景中,需要根据数据量、QPS、网络环境、团队技术能力等因素来选择合适的方案。
应用
如何保证缓存和数据库数据的一致性?
从理论上说,只要我们设置了合理的键的过期时间,我们就能保证缓存和数据库的数据最终是一致的。因为只要缓存数据过期了,就会被删除。随后读的时候,因为缓存里没有,就可以查数据库的数据,然后将数据库查出来的数据写入到缓存中。除了设置过期时间,我们还需要做更多的措施来尽量避免数据库与缓存处于不一致的情况发生。
新增、更改、删除数据库操作时同步更新 Redis,可以使用事务机制来保证数据的一致性。
一般有如下四种方案,详情看这里:
- 先更新数据库,后更新缓存
- 先更新缓存,后更新数据库
- 先删除缓存,后更新数据库
- 先更新数据库,后删除缓存
第一种和第二种方案,没有人使用的,原因如下:
- 第一种方案存在问题是:并发更新数据库场景下,会将脏数据刷到缓存。
- 第二种方案存在的问题是:如果先更新缓存成功,但是数据库更新失败,则肯定会造成数据不一致。
方案三、方案四参考如下两篇文章:
什么是缓存穿透?怎么解决?
缓存穿透是指当用户请求的数据不在缓存中,也不存在于数据库中时,每一次请求都需要直接访问数据库,从而导致缓存失效,给数据库带来巨大压力。由于数据不存在,所以也无法存储到缓存中,使得请求直接穿透缓存,访问数据库。
解决方案有如下几种:
- Bloom Filter(布隆过滤器):使用Bloom Filter来判断请求的数据是否存在,如果Bloom Filter表示该数据可能存在(注:Bloom Filter有一定误判率),则继续查询缓存和数据库,否则直接拦截请求。由于Bloom Filter可以高效地判断元素是否存在,且空间占用小,因此可以有效防止缓存穿透。
- 缓存空结果:当查询数据库后发现数据不存在时,仍然将此空数据或一个特殊标记存入缓存,并设置一个适当的过期时间。这样,后续对于同样的请求,缓存能直接返回结果,减轻数据库查询压力。需要注意的是这种方法中,如果设置过长的过期时间,可能会导致数据长时间不能被查询到。
- 限流:对频繁访问不存在的数据的请求,可以采用限流的策略来降低数据库的访问压力。比如可以设置时间窗口内对某一不存在数据的请求次数限制。


