基于Redis构建微服务的反应式架构 - bitsrc

21-11-05 banq

如何使用 Redis 的特性来生成反应式数据流?
Redis 是我遇到的最强大、最通用的技术之一。可悲的是,大多数人只知道它是因为它是一个很好的缓存解决方案。
我们需要解决这个问题。
特别是,我想向您展示您可以创建一个以 Redis 作为主要组件的反应式架构。这是一个巨大的优势,特别是如果您由于其他要求(即好的 ol'cache)已经将它作为基础架构的一部分。
您使用 Redis 与我将在此处描述的功能进行交互的方式取决于您,老实说,此时任何选项都与下一个选项一样有效。我倾向于使用 Node.js,但这就是我,你可以自由使用最适合你的东西。
 

构建反应式架构
这里首先要了解什么是反应式架构,为什么我们要构建一个而不是采用更传统的方法?
简而言之,反应式架构是在满足所有前提条件的那一刻开始执行每一位逻辑的架构——我想我应该在“简单”这个词周围加上引号。
让我换一种说法:当您需要在特定事件发生后触发逻辑时,您有两种选择:

  • 定期检查某种标志,直到它打开,这意味着事件发生了。
  • 坐等,直到其他东西通知您的服务该事件已触发。

第二部分是面向对象编程中观察者模式的关键。观察到的对象让对其内部状态感兴趣的每个人都知道,它实际上已更新。
我们在这里尝试做的是将相同的 OOP 模式外推到架构级设计中。因此,与其在我们的程序中加入一些逻辑,我说的是一旦正确的事件发生,就会触发服务的功能。
这是分发和扩展平台的最有效方式,因为:
  • 您不必浪费时间和网络流量轮询特定标志的数据源(或您认为应该轮询的任何内容)。此外,如果您使用的是按使用付费的基础架构,则不需要的轮询可能会导致额外费用,在目标服务上进行不必要的工作,并且如果在您的代码等待轮询期间发生多个事件,您可能最终不得不聚合事件.
  • 您可以通过添加新服务、并行工作和尽可能快地捕获事件来扩展服务的处理能力。
  • 平台更稳定。通过被动工作,您可以确保您的服务以最佳速度运行,而不必担心由于来自客户端的数据过载而崩溃。

反应式平台本质上是异步的,因此任何尝试使用它们的客户端应用程序也需要适应相同的范例。外部 API 可能是通过 HTTP 实现的 REST,但这并不意味着您将获得作为响应的答案,相反,您将获得一个200 OK响应,这意味着您的请求已被接收。为了让您的应用程序获得实际结果,它必须订阅将包含此类响应的特定事件。
请记住这一点,否则,您将花费很长时间来调试为什么没有得到您想要的响应。
 

那我们需要什么?
撇开这一点,我们需要什么才能使我们的平台/架构成为反应式平台/架构?它不是 ReactJS,这是肯定的。我们需要一个消息代理,它可以集中多个服务之间的消息分发。
有了可以充当代理的东西,我们需要确保我们的代码的编写方式可以通过让代理知道它在哪里以及它需要的事件类型来订阅某些事件。
之后,将向我们的服务发送通知,并触发我们的逻辑。
听起来很简单吧?那是因为它就是!
 

那么 Redis 是如何考虑的呢?
Redis 不仅仅是一个键值内存存储,事实上,它有 3 个我喜欢它的特性,允许我根据不同的预期行为创建反应式架构。
这3个特点是:

  • 发布/订阅。Redis 内部有一个消息队列,它允许我们发送消息并将它们分发给每个订阅的进程。这是一种即发即弃的合约,这意味着如果没有监听器处于活动状态,那么消息就会丢失。所以在使用这个频道时要考虑到这一点。
  • 键空间(KeySpace)通知。可能是我最喜欢的 Redis 功能。这些坏男孩是由 Redis 本身创建的事件,并分发给决定订阅它们的每个进程。它们与键空间的变化有关,这意味着您存储在其中的数据发生的任何事情。例如,当您删除或更新密钥时,或者当其 TTL 计数器达到 0 时自动删除。这允许您生成定时事件。在“某事”发生 3 天后,您是否必须触发一些逻辑?这是如何。
  • Redis 流。这是 Redis 数据类型的混合体,混合了键空间通知和发布/订阅,所有这些组合在一起并且运行良好。Streams 尝试模拟tail -f命令在您的终端上的行为。如果您从未见过该命令,那么这是一个 *nix 命令,它显示文件的最后一行,并持续监听文件的更改,以便在您添加新行时,它会立即列出。流也会发生同样的情况。考虑到正确的用例,它们非常强大且非常有用。您可以在此处阅读有关它们的更多信息

所有这些功能都允许您以一种或另一种方式与您的流程进行通信,并且根据您所追求的行为类型,您可能想要解决其中一个或全部。
让我们快速浏览一些示例,让您了解使用什么以及何时使用。
经典的基于事件的消息传递
最简单的例子是每个微服务都在等待某事发生。要触发的事件,该事件可能来自外部,即系统的用户或客户端。


看上图,把中央的红管看成是Redis的Pub/Sub或者Blocking list,这是一个更可靠的Pub/Sub的自定义实现。
流程从 #1 开始,由“客户端应用程序”提交请求,并在 9 点结束,“客户端应用程序”收到有关响应的通知。其余的部分?我不在乎,客户端应用程序也不应该在乎。
这是这种范式的优点之一,该架构成为客户端的黑匣子。单个请求可以触发数百个事件或仅触发一个,行为将是相同的:一旦响应准备就绪,它将被传递给客户端。而不是客户端知道需要多长时间或多久需要检查它是否准备好。这些都不重要。
请记住以下注意事项:

  • 消息由其订阅者发布到“通道”中。如果您想发布不同类型的主题,建议您拥有不同的频道。此外,如果您需要额外的粒度来区分哪个消费者必须负责处理一条特定的消息,则详细信息将需要成为消息的一部分。这是因为一个频道的所有订阅者都将获得相同的消息,因此如果您有多个进程侦听并获得相同的消息,您最终可能会重新执行相同的操作。可以在 Redis 中使用消息的 ID 实现一个标志(例如),以确保创建它的第一个进程是负责处理事件的进程,而其余进程可以忽略它。这是一种可靠的方法,因为在 Redis 中设置密钥是一个原子过程,
  • 如果没有订阅者收听某个特定频道,则发布的消息将丢失。如果您使用 Pub/Sub 模式就是这种情况,因为它在“即发即忘”机制下工作。如果您想确保您的邮件在处理之前一直保留在那里,您可以使用“阻止列表”方法。该解决方案包括直接在 Redis 的键空间上创建一个列表(即一个普通的值列表),并让进程订阅以获取有关该键的键空间通知。这样他们就可以决定如何处理插入的数据(即如果他们想忽略它,处理它并删除它等)。
  • 如果您要发送复杂的消息,例如 JSON,则需要对其进行序列化。这是因为对于阻止列表和 Pub/Sub,您唯一可以发送的是字符串。话虽如此,如果您需要在没有序列化的情况下通过线路发送复杂类型,您可以考虑使用 Redis Streams,这是它​​们允许的。当然,这里的限制是唯一允许的类型是 Redis,而不是您用来编写解决方案的语言的类型。

现在让我们看看如果您的事件触发取决于某些时间会发生什么。
 

基于时间的触发
反应式架构的另一个常见行为是能够在预定义的时间过去后触发某些事件。例如:在发现数据问题 10 分钟后触发警报。或者等待 30 分钟,然后触发 IoT 设备已停止发送数据的警报。
这些通常是与现实世界的限制相关的行为,需要一些时间来解决,或者甚至可以通过“稍等片刻”并重新开始倒计时来解决自己的问题(例如连接不可靠的物联网设备) )。
对于这种情况,架构保持不变,唯一的区别是中央通信集线器肯定使用来自 Redis键空间通知
您会看到,您需要了解有关 Redis 的两个主要功能才能实现这一目标:

  1. 设置键值对时,您可以选择以秒为单位定义 TTL(生存时间)。这就变成了倒计时,一旦达到 0,密钥将自动销毁。
  2. 当您订阅一个键空间时(这也适用于 pub/sub,但我们在这里没有使用它),您可以使用模式进行订阅。换句话说,您可以订阅“last_connection_time_of_device*”,而不是订阅键“last_connection_time_of_device100002”的事件。然后,一旦发生某些事情,创建的每个与该模式匹配的密钥都会通知您。

考虑到这两点,您可以创建订阅这些特定键的服务,并在它们被删除后(即事件被触发时)做出反应。同时,您让生产者不断更新密钥,这也会重置 TTL 计时器。因此,如果您正在跟踪设备上次发送其心跳的时间,则可以像我上面显示的那样为每个设备设置一个密钥,并在每次获得新的心跳时更新该密钥。一旦 TTL 结束,这意味着您在配置的时间内没有收到新的心跳。您订阅的进程将只接收密钥名称,因此如果您只需要设备的 ID,您可以像我展示的那样构造您的密钥,并解析名称以捕获所需的信息。
 

影键技术
另一方面,如果您在该键中保存了一个复杂的结构并且需要它,则必须稍微更改此方法。这是因为当 TTL 到期时,密钥将被删除,从而删除其中的数据,因此您无法真正检索它。这时,您可以使用一种称为“阴影键”的技术。
影子键本质上是用于触发事件的键,但这实际上是在隐藏包含您需要的数据的实际键。回到我们的例子,假设生产者每次收到心跳都会更新 2 个键:

  • “last_connection_time_of_device100002”带有最后从设备接收到的有效负载的unix时间戳。
  • “device_data_id100002”带有来自设备的额外信息。

在这两个键中,只有第一个也有 TTL,第二个没有。因此,当您收到过期通知时,您将从过期密钥 (last_connection_time_of_device100002) 中获取 ID 并使用它来读取第二个密钥的内容。然后,如果需要,您也可以继续删除其他密钥,或者将其保留在那里,无论对您的用例是否有效。
这里需要考虑的唯一警告是,如果您在集群模式下配置了 Redis,则密钥空间通知不会在整个集群中传播。这意味着您必须确保您的消费者连接到每个节点。其中一些通知会丢失,否则没有人会收到它们。这是这种技术的唯一缺点,但在您花几天时间调试异步逻辑之前了解它很重要(去过那里,做过)。
如您所见,两种情况下的复杂性都降低到只需确保您订阅正确的事件或分发渠道。例如,如果您要尝试使用普通 SQL 数据库执行此操作,则必须围绕代码创建大量逻辑,以有效确定何时输入新数据或何时删除一条信息。
相反,这里的所有复杂性都由 Redis 抽象出来,您需要担心的只是编写业务逻辑。那对我来说就是黄金。
 
您是否将 Redis 用于缓存之外的任何其他场景?在评论中与他人分享您的经验,我很想知道你们都是如何使用我最喜欢的技术之一!

1