System design

Understanding Redis

Posted by 許敲敲 on June 10, 2021

Understanding Redis in System Design

refer from author Nerd For Tech
在这篇文章中,我将解释系统设计中的Redis. 在一个依赖Redis作为设计主要部分的遗留系统中遇到了很多问题,在阅读和了解Redis之后,明白了这些问题.

What is a redis

Redis是一个开源的、基于内存数据结构存储,被用作数据库、缓存和消息代理。Redis提供的数据结构包括:字符串、哈希值、列表、集合、带范围查询的排序集合、位图、超日志、地理空间索引和流。Redis有内置的复制、Lua脚本、LRU eviction、事务和不同级别的磁盘持久性,并通过Redis Sentinel和Redis Cluster的自动分区提供高可用性。 因此,Redis可以作为一个传统的单体使用,也可以作为分布式系统使用,作为一个有分片的节点集群。 在谈论Redis之前,我将解释一些概念,以及为什么我们在系统中需要这些术语和技术。

What is memory caching and what is the value-added by caching?

缓存就像短期记忆。它通常比原始数据源更快。从内存中访问数据比从硬盘中访问要快。缓存意味着将经常访问的数据保存在内存中(短期内存),所以缓存所增加的价值是快速检索数据并减少对原始数据源的调用,它可能是SQL DB,因为读取数据的复杂时间将是O(1),像hashtables那样通过内存中的键直接访问操作。

因此,Redis是一个为我们提供单片机和分布式环境下的缓存系统的系统。

How Redis works?

所有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文件将被用来重建原始数据集,因为它被保证是最完整的。

因此,这里最重要的部分是理解RDBAOF持久性之间的权衡,因为无持久性是非常明确的,没有任何级别的一致性存在,甚至是强或坏的一致性。

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个变化,或者你可以手动调用SAVEBGSAVE命令。

工作原理

  • Redis fork。我们现在有一个子进程和一个父进程。
  • 子进程开始将数据集写入一个临时的RDB文件中。
  • 当子进程写完新的RDB文件后,它将取代旧的RDB文件。

所以Redis在以下情况下将数据的快照存储到磁盘的dump.rdb文件中。

  1. Every minute if 1000 keys were changed
  2. Every 5 minutes if 10 keys were changed
  3. Every 15 minutes if 1 key was changed 因此,如果你正在频繁的增删改数据,改变了很多key,那么Redis每分钟将为你生成一个快照,如果你的改变不是那么多,那么每5分钟一个快照,如果真的不是那么多,那么每15分钟一个快照。

Append-only file

快照不是很持久。如果你运行Redis的电脑停止运行,你的电源线发生故障,或者你不小心kill-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 fork,所以现在我们有一个子进程和一个父进程。
  • 子进程开始在一个临时文件中写入新的AOF。
  • 父进程在一个内存缓冲区中积累所有的新变化(但同时它也将新变化写入旧的append-only文件中,所以如果重写失败,我们是安全的)。
  • 当子代完成重写文件时,父代得到一个信号,并将内存缓冲区追加到子代生成的文件的末尾。
  • 以上步骤完成, Redis原子地将旧文件重命名为新文件,并开始将新数据追加到新文件中。

因此,我们可以理解,Redis在任何模式下都不能保证一致性,因为向磁盘的写入总是由引擎以异步方式完成,如果在数据同步之前发生崩溃,你很可能会丢失数据,你可以减少这种情况,但你不能防止。

可用性

很明显,单体的Redis不能保证任何级别的可用性,因为单体意味着单点故障,所以让我们解释一下Redis的其他模式。 Redis Cluster提供了一种运行Redis安装的方法,其中数据被自动分散到多个Redis节点。

redis cluster

Redis Cluster在分区期间也提供了一定程度的可用性,也就是在实际操作中,当一些节点失败或无法通信时,能够继续操作。然而,在更大的故障情况下,集群会停止运行(例如,当大多数主控器不可用时)。

那么,在实践中,Redis Cluster可以

  • 能够在多个节点之间自动分割你的数据集。
  • 当一个子集的节点遇到故障或无法与集群的其他部分通信时,能够继续操作。

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 master slave

Redis Cluster的一致性保证?

一致性始终是非常重要的,但正如我们所解释的,Redis不能保证一致性,Redis Cluster也不能保证强一致性。在实践中,这意味着在某些条件下,Redis Cluster有可能会丢失系统确认给客户端的写入。 Redis Cluster可能丢失写入的第一个原因是,它使用了异步复制。这意味着在写的过程中,会发生以下情况。

  • 你的客户写到主站B。
  • 主站B对你的客户端作出确定的回复。
  • 主站B将写入的内容传播给它的从站B1、B2和B3。 正如你所看到的,B不会等待B1、B2、B3的确认,然后再回复给客户端,因为这对Redis来说是一个过高的延迟,所以如果你的客户端写了一些东西,B确认了写,但在将写发送给它的从站之前崩溃了,其中一个从站(没有收到写)可以被提升为主站,永远失去了写。

我们怎样才能提高Redis的一致性水平

Redis企业软件(RS)具有将数据复制到另一个从机上以获得高可用性的能力,并将内存中的数据永久地保存在磁盘上以获得耐久性。通过WAIT命令,你可以控制RS中复制和持久化的数据库的一致性和耐久性保证。 redis consistency 通过WAIT命令,应用程序可以要求在复制或持久化在从属设备上得到确认后才等待确认。使用WAIT命令的写操作的流程如下所示。

  1. 应用程序发出一个写操作。
  2. 代理与系统中包含给定密钥的正确主 “分片 “进行通信。
  3. 复制将更新传达给从属分片。
  4. 从站将更新持续到磁盘(假设选择了AOF每次写入设置)。
  5. 确认从从属区一路发回给代理区,步骤为5到8。

但是请注意,WAIT并不能使Redis成为一个强一致性的存储:虽然同步复制是复制状态机的一部分,但它并不是唯一需要的东西。然而在Sentinel或Redis Cluster故障转移的情况下,WAIT改善了现实世界的数据安全。具体来说,如果一个给定的写被转移到一个或多个副本,那么更有可能(但不能保证)的是,如果主站发生故障,我们将能够在故障切换期间推广收到写的副本:Sentinel和Redis Cluster都会尽最大努力尝试在可用副本集合中推广最佳副本。然而,这只是一个尽力的尝试,所以仍然有可能丢失同步复制到多个副本的写。

在谈论可扩展性方面,我也要解释一下分区这个词.

分区是将你的数据分成多个Redis实例的过程,这样每个实例将只包含你的键的一个子集。本文的第一部分将向你介绍分区的概念,第二部分将向你展示Redis分区的替代方案。

分区的好处是什么? Redis中的分区有两个主要目标。

  • 它允许更大的数据库,使用许多计算机的内存之和。如果没有分区,你将被限制在一台计算机所能支持的内存量上。
  • 它允许将计算能力扩展到多个核心和多台计算机,并将网络带宽扩展到多台计算机和网络适配器。

分区的不同实现方式。

分区可以由软件栈的不同部分负责。

  • 客户端分区意味着客户端直接选择正确的节点来写入或读取一个给定的键。许多Redis客户端实现了客户端分区。
  • 代理协助的分区意味着我们的客户将请求发送到一个能够讲Redis协议的代理,而不是将请求直接发送到正确的Redis实例。代理将确保根据配置的分区模式将我们的请求转发给正确的Redis实例,并将回复发回给客户端。RedisMemcached代理Twemproxy实现了代理辅助的分区。
  • 查询路由意味着你可以把你的查询发送给一个随机的实例,而该实例将确保把你的查询转发给正确的节点。Redis Cluster实现了一种混合形式的查询路由,在客户端的帮助下(请求不是直接从一个Redis实例转发到另一个,但客户端会被重定向到正确的节点)。

分区的劣势

  • 通常不支持涉及多个键的操作。
  • 分区的粒度是关键,所以不可能用一个巨大的键来共享一个数据集,比如一个非常大的排序集。
  • 当使用分区时,数据处理会更加复杂,例如,你必须处理多个RDB / AOF文件,为了对数据进行备份,你需要聚合多个实例和主机的持久化文件。
  • 增加和删除容量可能很复杂。例如,Redis Cluster支持大部分透明的数据再平衡,能够在运行时添加和删除节点,但其他系统如客户端分区和代理不支持这个功能。不过,一种叫做预分片的技术在这方面有帮助。

集群槽设置

正如我们之前解释的,Redis集群根据槽来分配数据。

  • CLUSTER SETS LOT负责以不同方式改变接收节点中哈希槽的状态。它可以,取决于使用的子命令。
  • MIGRATING子命令。将一个哈希槽设置为迁移状态。
  • IMPORTING子命令。在导入状态下设置一个哈希槽。
  • STABLE子命令。清除散列槽中的任何导入/迁移状态。
  • NODE子命令。将哈希槽绑定到一个不同的节点上。

MIGRATING

这个子命令将一个槽位设置为迁移状态。为了将一个槽设置为这种状态,接收命令的节点必须是哈希槽的所有者,否则会返回一个错误。当一个槽被设置为迁移状态时,节点以如下方式改变行为。

  1. 如果收到关于一个现有密钥的命令,该命令将被照常处理。
  2. 如果收到关于一个不存在的密钥的命令,节点会发出ASK重定向,要求客户端只重试对目标节点的特定查询。在这种情况下,客户端不应该更新其哈希槽到节点的映射。
  3. 如果命令包含多个键,在不存在的情况下,行为与第2点相同,如果全部存在,则与第1点相同,但是,如果只有部分键存在,命令会发出TRYAGAIN错误,以使感兴趣的键完成向目标节点的迁移,从而使多键命令能够被执行。

IMPORTING

这个子命令与 MIGRATING 相反,它为目标节点从指定的源节点导入密钥做准备。该命令只有在节点尚未成为指定哈希槽的所有者时才有效。 当一个槽被设置为导入状态时,节点会以如下方式改变行为。

  1. 关于这个哈希槽的命令会被拒绝,并且像往常一样产生MOVED重定向,但是在这种情况下,该命令是在 ASKING 命令之后,在这种情况下,该命令会被执行。

这样,当一个处于迁移状态的节点产生ASK重定向时,客户端联系目标节点发送,之后立即发送命令。这样一来,关于旧节点中不存在的key或已经迁移到目标节点的key的命令就会在目标节点中执行,因此:

  • 新的键总是在目标节点中创建。在哈希槽迁移过程中,我们只需要移动旧的键,而不是新的。
  • 为了保证一致性,关于已经迁移的键的命令会在迁移的目标节点,即新的哈希槽所有者的上下文中正确处理。
  • 如果没有ASKING,则行为与平时相同。这就保证了有破损的哈希槽映射的客户端不会在目标节点上写出错误,创建一个尚未被迁移的钥匙的新版本。

stable

这个子命令只是清除了插槽中的迁移/导入状态。

Node

NODE这个子命令是语义最复杂的一个命令。它将哈希槽与指定的节点联系起来,然而,该命令只在特定情况下起作用,并且根据槽的状态有不同的副作用。下面是该命令的一组前提条件和副作用。

  1. 如果当前哈希槽的所有者是接收命令的节点,但为了命令的效果,该槽将被分配给不同的节点,如果接收命令的节点中还有该哈希槽的键,那么命令将返回一个错误。
  2. 如果槽处于迁移状态,当槽被分配到另一个节点时,该状态会被清除。
  3. 如果槽在接收命令的节点中处于导入状态,而命令将槽分配给了这个节点(这发生在目标节点中,在一个哈希槽从一个节点重分到另一个节点的结束时),命令有以下副作用。 A)导入的状态被清除了。 B)如果节点的config epoch还不是集群的最大epoch,它就会生成一个新的epoch,并将新的配置epoch分配给自己。这样它的新哈希槽所有权就会胜过以往的故障转移或槽位迁移所产生的任何配置。

Redis集群和容错。

一个系统在面对故障时继续运行的能力被称为容错。故障可以是以下几种情况之一 节点故障 网络故障 特定应用的故障

参考:Distributed system models