在这篇文章中,我将解释系统设计中的Redis,因为我在一个依赖Redis作为设计主要部分的遗留系统中遇到了很多问题,在阅读和了解Redis之后,我明白了这些问题。
什么是Redis?
Redis是一个开源的、内存中的数据结构存储,用作数据库、缓存和消息代理。Redis提供的数据结构包括:字符串、哈希值、列表、集合、带范围查询的排序集合、位图、超日志、地理空间索引和流。Redis有内置的复制、Lua脚本、LRU驱逐、事务和不同级别的磁盘持久性,并通过Redis Sentinel和Redis Cluster的自动分区提供高可用性。
因此,Redis可以作为一个传统的单片机使用,也可以作为分布式系统使用,作为一个有分片的节点集群。
在谈论Redis之前,我将解释一些概念,以及为什么我们在系统中需要这些术语和技术。
什么是内存缓存,缓存的增值作用是什么?
A 缓存就像短期内存。它通常比原始数据源更快。从内存中访问数据比从硬盘中访问要快。缓存意味着将经常访问的数据保存在内存中(短期内存),所以缓存所增加的价值是快速检索数据并减少对原始数据源的调用,它可能是SQL DB,因为读取数据的复杂时间将是O(1),像hashtables那样通过内存中的键直接访问操作。
因此,Redis是一个在单片机和分布式环境中为我们提供缓存系统的系统。
Redis是如何工作的?
所有Redis的数据都驻留在服务器的主内存中,与PostgreSQL、SQL Server等数据库相比,这些数据库将大部分数据存储在磁盘上。与传统的基于磁盘的数据库相比,大多数操作需要往返于磁盘,像Redis这样的内存数据存储不会受到同样的惩罚。因此,它们可以支持多数量级的操作和更快的响应时间。其结果是--极快的性能,平均读或写的操作时间不到一毫秒,并支持每秒数百万次的操作。
那么,由于Redis比传统的数据库更快,我们可以认为它是第一真理的来源吗?
答案是否定的 ,我们不能将Redis视为第一真理来源,它总是作为第二支持来提高系统的性能,因为从CAP定理的角度来看,Redis既不是高度可用的,也不是一致的。要理解为什么,让我们解释一下Redis如何将数据从内存同步到磁盘,因为磁盘可以考虑一致性。
正如我们之前解释的那样,Redis的数据存在于内存中,这使得它的写入和读取速度非常快,但在服务器崩溃的情况下,你会失去内存中的所有数据,对于一些应用程序来说,在崩溃的情况下失去这些数据是可以的,但对于其他应用程序来说,在服务器重新启动后能够重新加载Redis数据是非常重要的。
所以Redis提供了不同范围的持久化选项。
- RDB(Redis数据库)。RDB持久化在指定的时间间隔内对你的数据集进行时间点快照。
- AOF(Append Only File)。AOF持久性记录了服务器收到的每一个写操作,这些操作将在服务器启动时再次播放,重建原始数据集。命令的记录采用与Redis协议本身相同的格式,以只附加的方式进行。Redis能够在后台重写日志,当它变得太大时。
- 没有持久性。如果你愿意,你可以完全禁用持久性,如果你希望你的数据只要服务器运行就存在。
- RDB + AOF:可以在同一个实例中结合AOF和RDB。注意,在这种情况下,当Redis重新启动时,AOF文件将被用来重建原始数据集,因为它被保证是最完整的。
因此,那里最重要的部分是理解RDB和AOF持久性之间的权衡,因为无持久性是非常 ,没有任何级别的一致性,甚至强或坏的一致性。
RDB的优势:
- RDB是你的Redis数据的一个非常紧凑的单文件时间点表示。RDB文件是备份的完美选择。例如,你可能想在最近24小时内每小时归档你的RDB文件,并在30天内每天保存一个RDB快照。这允许你在发生灾难时轻松恢复数据集的不同版本。
- RDB对于灾难恢复是非常好的,它是一个单一的紧凑文件,可以被转移到远处的数据中心。
- 与AOF相比,RDB允许在大数据集下更快地重新启动
RDB的缺点。
- 如果你需要在Redis停止工作的情况下将数据丢失的几率降到最低(例如停电后),那么RDB就不是很好。你可以配置不同的_保存点_,在那里产生RDB(例如,在至少5分钟和针对数据集的100次写入之后,但你可以有多个保存点)。然而,你通常会每五分钟或更长时间创建一个RDB快照,所以在Redis因任何原因停止工作而没有正确关闭的情况下,你应该准备好失去最近几分钟的数据。
AOF的优势。
- 使用AOF Redis更持久:你可以有不同的fsync策略:完全没有fsync,每秒钟fsync,每次查询都fsync。在默认的每秒钟一次的fsync策略下,写入性能仍然很好(fsync是使用后台线程执行的,当没有fsync时,主线程会努力执行写入。)但你只能损失一秒钟的写入量。
- AOF日志是一个仅有附录的日志,所以不存在寻求,也不存在断电时的损坏问题。即使由于某种原因(磁盘满了或其他原因),日志以写了一半的命令结束,Redis-check-of工具也能轻易地修复它。
- 当AOF过大时,Redis能够在后台自动重写。重写是完全安全的,因为当Redis继续对旧文件进行追加时,一个全新的文件会以创建当前数据集所需的最小操作集产生,一旦这第二个文件准备好了,Redis就会切换这两个文件并开始对新文件进行追加。
- AOF包含了所有操作的日志,一个接一个,而且是以易于理解和解析的格式。你甚至可以很容易地导出一个AOF文件。例如,即使你不小心使用FLUSHALL命令刷新了所有东西,只要在此期间没有对日志进行重写,你仍然可以保存你的数据集,只需停止服务器,删除最新的命令,然后再次重启Redis。
AOF的缺点。
- AOF文件通常比相同数据集的同等RDB文件大。
- AOF可能比RDB慢,这取决于确切的fsync策略。
- 最后,AOF可以提高数据的一致性,但不能保证,所以你可能会丢失你的数据,但考虑到RDB更快,所以比RDB模式要少。
我应该使用什么?
这在任何系统设计中都是一样的,但一般来说,如果你想获得与PostgreSQL所提供的数据安全程度相当的数据,你应该使用这两种持久化方法。如果你非常关心你的数据,但仍然可以忍受在发生灾难时几分钟的数据丢失,你可以简单地单独使用RDB。
在我们解释了Redis的数据存储机制之后,让我们来解释两个重要的持久化模型。
快照。
默认情况下,Redis将数据集的快照保存在磁盘上,在一个叫做dump.rdb的二进制文件中。你可以配置Redis,让它每隔N秒保存一次数据集,如果数据集中至少有M个变化,或者你可以手动调用SAVE或BGSAVE命令。
它是如何工作的。
- Redis分叉。我们现在有一个子进程和一个父进程。
- 子进程开始将数据集写入一个临时的RDB文件中。
- 当子进程写完新的RDB文件后,它将取代旧的RDB文件。
所以Redis在以下情况下将数据的快照存储到磁盘的dump.rdb文件中。
- 如果有1000个键被改变,每分钟一次
- 如果有10个键被改变,每5分钟一次
- 每15分钟,如果有一个键被改变
因此,如果你正在做繁重的工作,改变了很多钥匙,那么每分钟将为你生成一个快照,如果你的改变不是那么多,那么每5分钟一个快照,如果真的不是那么多,那么每15分钟一个快照将被提取。
仅仅是附加文件。
快照不是很持久。如果你运行 Redis 的计算机停止运行,你的电源线发生故障,或者你不小心杀死了 -9 你的实例,那么写在 Redis 上的最新数据将丢失。虽然这对某些应用来说可能不是什么大问题,但有些用例需要完全的耐久性,在这些情况下,Redis并不是一个可行的选择。_仅附加文件_是Redis的一个替代性的、完全持久的策略。它在1.1版本中开始使用。你可以在你的配置文件中打开AOF。
appendonly yes
只做附录的文件有多耐用?
正如我们在AOF部分所解释的,我们有以下耐用性级别的选项
- appendfsync always:每次有新的命令被附加到AOF上时都会进行fsync。非常非常慢,非常安全。注意,命令是在一批来自多个客户端或管道的命令被执行后追加到AOF的,所以这意味着一次写和一次fsync(在发送回复之前)。
- appendfsync everysec: 每秒钟进行一次fsync。足够快(在2.4中可能和快照一样快),如果发生灾难,你会损失1秒的数据。
- appendfsync no: 永远不要fsync,只是把你的数据放在操作系统的手中。更快也更不安全的方法。通常情况下,Linux在这种配置下会每30秒刷新一次数据,但这取决于内核的精确调校。
它是如何工作的。
- Redis分叉,所以现在我们有一个子进程和一个父进程。
- 子进程开始在一个临时文件中写入新的AOF。
- 父进程在内存缓冲区中积累所有的新变化(但同时它也在旧的append-only文件中写入新变化,所以如果重写失败,我们是安全的)。
- 当子代完成重写文件时,父代得到一个信号,并将内存缓冲区追加到子代生成的文件的末尾。
- 获利了!现在Redis原子式地将旧文件重命名为新文件,并开始将新数据追加到新文件中。
因此,我们可以理解,Redis在任何模式下都不能保证一致性,因为写入磁盘总是由引擎以异步方式完成的,如果在数据同步之前发生崩溃,你很可能会丢失数据,你可以减少这种情况,但你不能防止。
可用性如何?
很明显,单体的Redis不能保证任何级别的可用性,因为单体意味着单点故障,所以让我们解释一下Redis的其他模式。
Redis Cluster提供了一种运行Redis安装的方法,其中数据被自动分散到多个Redis节点。
Redis Cluster还在分区期间提供了一定程度的可用性,也就是在实际操作中,当一些节点失败或无法通信时,能够继续操作。然而,在更大的故障情况下,集群会停止运行(例如,当大多数主控器不可用时)。
那么,在实践中,你能从Redis集群中得到什么呢?
- 能够在多个节点之间自动分割你的数据集。
- 当一个子集的节点遇到故障或无法与集群的其他部分通信时,能够继续操作。
Redis Cluster的分布式存储?
Redis Cluster不使用一致的散列,而是使用不同形式的分片,其中每个密钥在概念上是我们称之为散列槽的一部分。Redis Cluster中有16384个哈希槽,要计算一个给定密钥的哈希槽是什么,我们只需将该密钥的CRC16调制为16384。Redis集群中的每个节点都负责一个哈希槽的子集,因此,例如你可能有一个有3个节点的集群,其中。
- 节点A包含从0到5500的哈希槽。
- 节点B包含从5501到11000的哈希槽。
- 节点C包含从11001到16383的哈希槽。
这允许在集群中轻松添加和删除节点(规模),并且不需要任何停机时间。
Redis集群的主从模式(Redis故障转移)?
为了在一个主节点子集失效或无法与大多数节点通信时保持可用,Redis集群使用主从模型,每个哈希槽有1(主节点本身)到N个副本(N-1个额外的从属节点)。在我们有节点A、B、C的例子集群中,如果节点B失效,集群就无法继续,因为我们不再有办法为5501-11000范围内的哈希槽服务。然而,当集群创建时(或在以后的时间),我们在每个主节点上添加一个从属节点,这样,最终的集群由A、B、C组成,它们是主节点,A1、B1、C1是从属节点。这样一来,如果节点B发生故障,系统就能继续运行。
Redis集群的一致性保证?
一致性总是非常重要的,但正如我们所解释的Redis不能保证一致性,Redis Cluster也不能保证强一致性。在实践中,这意味着在某些条件下,Redis Cluster有可能会丢失系统确认给客户端的写入。
Redis Cluster可能丢失写入的第一个原因是,它使用了异步复制。这意味着在写的过程中,会发生以下情况。
- 你的客户写到主站B。
- 主站B对你的客户端作出确定的回复。
- 主站B将写入的内容传播给它的从站B1、B2和B3。
正如你所看到的,B不会等待B1、B2、B3的确认,然后再回复给客户端,因为这对Redis来说是一个巨大的延迟惩罚,所以如果你的客户端写了一些东西,B确认了写,但在能够将写发送给它的从站之前崩溃了,其中一个从站(没有收到写)可能被提升为主站,永远失去了写。
我们怎样才能提高Redis的一致性水平?
Redis企业软件(RS)有能力将数据复制到另一个从机上以获得高可用性,并将内存中的数据永久地保存在磁盘上以获得持久性。通过WAIT命令,你可以控制RS中复制和持久化的数据库的一致性和耐久性保证。
通过WAIT命令,应用程序可以要求只在复制或持久化在从属系统上得到确认后等待确认。使用WAIT命令的写操作的流程如下所示。
- 应用程序发出一个写操作。
- 代理与系统中包含给定密钥的正确主 "分片 "进行通信。
- 复制将更新传达给从属分片。
- 从站将更新持续到磁盘(假设选择了AOF每次写入设置)。
- 确认书从slave一路发回给代理,步骤是5到8。
但是 请注意,WAIT并不能使Redis成为一个强一致性的存储:虽然同步复制是复制状态机的一部分,但它并不是唯一需要的东西。然而在Sentinel或Redis Cluster故障转移的情况下,WAIT改善了现实世界的数据安全。具体来说,如果一个给定的写被转移到一个或多个副本,那么更有可能(但不能保证)的是,如果主站发生故障,我们将能够在故障切换期间推广收到写的副本:Sentinel和Redis Cluster都会尽最大努力尝试在可用副本集合中推广最佳副本。然而,这只是一个最好的尝试,所以仍然有可能失去一个同步复制到多个副本的写。
在谈到可扩展性时,我也要解释一下分区这个词
分区是将你的数据分割到多个Redis实例的过程,这样每个实例将只包含你的键的子集。本文的第一部分将向你介绍分区的概念,第二部分将向你展示Redis分区的替代方案。
分区的好处是什么?
Redis中的分区有两个主要目标。
- 它允许更大的数据库,使用许多计算机的内存之和。如果没有分区,你将被限制在一台计算机所能支持的内存量上。
- 它允许将计算能力扩展到多个核心和多台计算机,并将网络带宽扩展到多台计算机和网络适配器。
分区的不同实现方式。
分区可以由软件栈的不同部分负责。
- 客户端分区意味着客户端直接选择正确的节点,在那里写入或读取一个给定的键。许多Redis客户端实现了客户端分区。
- 代理协助的分区意味着我们的客户将请求发送到一个能够讲Redis协议的代理,而不是将请求直接发送到正确的Redis实例。代理将确保根据配置的分区模式将我们的请求转发给正确的Redis实例,并将回复发回给客户端。Redis和Memcached代理Twemproxy实现了代理辅助的分区。
- 查询路由意味着你可以把你的查询发送给一个随机的实例,而该实例将确保把你的查询转发给正确的节点。Redis Cluster实现了一种混合形式的查询路由,在客户端的帮助下(请求不是直接从一个Redis实例转发到另一个,但客户端会被_重定向_到正确的节点)。
分区的劣势。
- 通常不支持涉及多个键的操作。
- 分区的粒度是键,所以不可能用一个巨大的键共享一个数据集,比如一个非常大的排序集。
- 当使用分区时,数据处理更加复杂,例如,你必须处理多个RDB / AOF文件,为了对数据进行备份,你需要聚合多个实例和主机的持久化文件。
- 增加和删除容量可能很复杂。例如,Redis Cluster支持大部分透明的数据再平衡,能够在运行时添加和删除节点,但其他系统如客户端分区和代理不支持这个功能。不过,一种叫做_预分片_的技术在这方面有帮助。
集群设置槽。
正如我们之前解释的那样,Redis集群根据槽位来分配数据。
CLUSTER SETS LOT负责以不同方式改变接收节点中哈希槽的状态。它可以,取决于使用的子命令。
- MIGRATING子命令。将一个哈希槽设置为_迁移_状态。
- IMPORTING子命令。在_导入_状态下设置一个哈希槽。
- STABLE子命令。清除散列槽中的任何导入/迁移状态。
- NODE子命令。将哈希槽绑定到一个不同的节点上。
迁移。
这个子命令将一个槽位设置为_迁移_状态。为了将槽设置为这种状态,接收命令的节点必须是哈希槽的所有者,否则会返回一个错误。当一个槽被设置为迁移状态时,节点以如下方式改变行为。
- 如果收到关于一个现有密钥的命令,该命令会被照常处理。
- 如果收到关于一个不存在的密钥的命令,节点会发出ASK重定向,要求客户端只重试对目标节点的特定查询。在这种情况下,客户端不应该更新其哈希槽到节点的映射。
- 如果命令包含多个键,在不存在的情况下,行为与第2点相同,如果全部存在,则与第1点相同,然而,如果只有部分数量的键存在,命令会发出TRYAGAIN错误,以使感兴趣的键完成迁移到目标节点,从而使多键命令可以被执行。
导入。
这个子命令与MIGRATING相反,它为目标节点从指定的源节点导入钥匙做准备。该命令只有在该节点尚未成为指定哈希槽的所有者时才有效。
当一个槽被设置为导入状态时,节点会以如下方式改变行为。
- 关于这个哈希槽的命令会被拒绝,并且像往常一样产生MOVED重定向,但是在这种情况下,该命令是在ASKING命令之后,在这种情况下,该命令被执行。
这样,当一个处于迁移状态的节点产生ASK重定向时,客户端联系目标节点发送,之后立即发送命令。这样一来,关于旧节点中不存在的钥匙或已经迁移到目标节点的钥匙的命令就会在目标节点中执行,这样。
- 新的键总是在目标节点中被创建。在哈希槽迁移过程中,我们只需要移动旧的键,而不是新的。
- 为了保证一致性,关于已经迁移的键的命令会在迁移的目标节点,即新的哈希槽所有者的上下文中正确处理。
- 如果没有ASKING,则行为与平时相同。这就保证了有破损的哈希槽映射的客户端不会在目标节点上写出错误,创建一个尚未被迁移的钥匙的新版本。
稳定。
这个子命令只是清除了槽中的迁移/导入状态
Node。
NODE这个子命令是语义最复杂的一个命令。它将哈希槽与指定的节点联系起来,然而,该命令只在特定情况下起作用,并且根据槽的状态有不同的副作用。下面是该命令的一组前提条件和副作用。
- 如果当前的哈希槽所有者是接收命令的节点,但为了命令的效果,该槽将被分配给不同的节点,如果接收命令的节点中还有该哈希槽的键,那么命令将返回一个错误。
- 如果槽处于_迁移_状态,当槽被分配到另一个节点时,该状态会被清除。
- 如果槽在接收命令的节点中处于_导入_状态,而命令将槽分配给了这个节点(这发生在目标节点中,在一个哈希槽从一个节点重分到另一个节点的结束时),命令有以下副作用。A)_导入的_状态被清除了。B)如果节点的配置纪元还不是集群的最大纪元,它就会生成一个新的纪元,并将新的配置纪元分配给自己。这样它的新哈希槽所有权就会赢过以前的故障转移或槽迁移所产生的任何过去的配置。
Redis集群和容错。
一个系统在面对故障时继续运行的能力被称为容错。故障可以是以下几种情况之一
- 节点故障
- 网络故障
- 特定应用的故障
我在这篇文章中还解释了关于这些行为的更多细节
Redis的容错性取决于集群的故障切换能力,因此并非所有的Redis设置都能保证容错性
1- 带有节点主控的Redis集群
考虑这个Redis主控器和哨兵的设置。较大的盒子是一个节点。较小的盒子可以被认为是一个容器。如果主进程死亡,哨兵将检测到该进程已经死亡。它将知道主进程已经停机。然而,它将无法使服务重新启动。所以这种设置不是容错的。
2- 该设置由两个实例组成,一个为主,一个为辅。如果主进程死亡,那么哨兵可以检测到它并促进从属进程。这种设置可以容忍主进程的失败。然而,如果运行主进程的节点死亡,那么M1和S1都会死亡。在这种情况下,剩下的哨兵将无法执行故障转移,因为它需要总哨兵的大多数同意新的主站。这种设置将不能容忍主节点的失败.
3- 该设置包括三个实例,一个主实例和两个从实例。每个实例都在不同的节点上运行。如果主进程死亡,或者主节点死亡,可以进行多数投票,其中一个从属实例可以被提升为新的主进程。这种设置可以容忍主进程失败和主节点失败,但这种设置不能保证在一个主节点和一个从节点故障时的容错。
最后,我们有以下结果。
1- Redis在任何条件和配置下都是不稳定的。
2- Redis不是一个传统的数据库,如果一致性很重要的话,我们不能把它作为第一真理来源。
3- Redis总是作为强一致性数据库系统(如SQL数据库)的第二支持来提高性能。
4- Redis通过带有故障检测的集群模式提供自动故障转移。
5- 并非所有Redis模式都能保证容错性
6- Redis集群与分片只在你想保证高可用性时才需要,考虑到数据丢失和复杂性,如果你的数据不超过一个节点的内存,你就不需要这个。
7- 在使用技术和工艺之前,一定要阅读其缺点。
8- 系统设计需要权衡利弊,所以你应该深思熟虑,因为从你的系统中删除一些组件可能会非常昂贵。
参考资料。
了解系统设计中的Redis》最初发表在《Nerd For Tech》杂志上,人们通过强调和回应这个故事来继续对话。