使用Redis的功能来生成一个反应式数据流
Redis是我遇到过的最强大、最通用的技术之一。可悲的是,大多数人知道它只是因为它是一个好的缓存解决方案。
我们需要解决这个问题。
特别是,我想告诉你,你可以创建一个以Redis为主要组件的反应式架构。这是一个巨大的优势,特别是如果你已经有了它作为你的基础设施的一部分,由于其他要求(即好的缓存)。
你如何使用Redis与我在这里描述的功能进行交互取决于你,说实话,在这一点上任何选择都是有效的。我倾向于使用Node.js,但这就是我,你可以自由地使用最适合你的东西。
构建一个反应式架构
首先要了解的是什么是反应式架构,以及为什么我们要建立一个反应式架构而不是采用更传统的方法?
简单地说,一个反应式架构就是每一个逻辑都在满足所有前提条件的时候被执行--我想我应该在 "简单 "这个词周围加上引号。
让我换个说法:当你需要你的逻辑在某个特定事件发生后被触发时,你有两个选择。
- 定期检查某种标志,直到它被打开,意味着事件发生。
- 坐着等待,直到其他东西通知你的服务,事件被触发。
第二部分是面向对象编程中观察者模式的关键。被观察的对象让所有对其内部状态感兴趣的人知道,事实上,它被更新了。
我们在这里要做的,是将同样的OOP模式推导到架构级设计中。因此,我说的不是在我们的程序内有一点逻辑,而是一旦有正确的事件发生,就会触发服务的功能。
这是分布和扩展平台的最有效的方式,由于这样的事实。
- 你不必浪费时间和网络流量去轮询一个数据源的特定标志(或者你觉得应该轮询的东西)。此外,如果你是在一个按使用付费的基础设施上,不需要的轮询可能会导致额外的开支,在目标服务上不需要的工作,如果在你的代码等待轮询的时间里发生了不止一个事件,你最终可能不得不聚合事件。
- 你可以通过添加新的服务来扩大服务的处理能力,并行工作,并尽可能快地捕获事件。
- 该平台更稳定。通过反应式工作,你可以确保你的服务将以最佳速度工作,而不用担心由于客户端的数据过载而崩溃。
反应式平台本质上是异步的,所以任何试图与之合作的客户端应用程序,也需要适应同样的范式。外部API可能是通过HTTP的REST,但这并不意味着你会得到你的答案,相反,你会得到一个200 OK的响应,意味着你的请求被收到。为了让你的应用程序得到实际的结果,它必须订阅包含这种响应的特定事件。
记住这一点,否则,你会花很长时间来调试为什么你没有得到你想要的响应。
那我们需要什么呢?
说完了这些,我们需要什么来使我们的平台/架构成为一个反应式的平台/架构?这不是ReactJS,这是肯定的。我们需要一个消息代理,一个能在多个服务之间集中分配消息的东西。
有了可以充当经纪人的东西,我们需要确保我们的代码写得可以订阅某些事件,让经纪人知道它在哪里,以及它需要的事件类型。
之后,一个通知将被发送到我们的服务,我们的逻辑将被触发。
听起来很简单吧?那是因为它是!
那么Redis在这里是如何发挥作用的呢?
Redis不仅仅是一个键值内存存储,事实上,它有3个我喜欢的功能,允许我根据不同的预期行为来创建反应式架构。
这3个功能是。
- Pub/Sub。Redis内部有一个消息队列,它允许我们发送消息,并将其分发到每个订阅的进程。这是一个火与忘的合同类型,这意味着如果没有活跃的监听器,那么消息就会丢失。所以在使用这个通道时要考虑到这一点。
- 密钥空间通知。可能是我最喜欢的Redis的功能。这些坏小子是由Redis自己创建的事件,并分发给每个决定订阅它们的进程。它们是关于钥匙空间的变化的,也就是说,任何发生在你存储在里面的数据上的事情。例如,当你删除或更新一个密钥时,或者当它的TTL计数器达到0时自动删除。你是否曾经需要在 "某事 "发生3天后触发一个逻辑?这就是方法。
- Redis流。这是Redis的数据类型的混合物,混合了钥匙空间通知和pub/sub,全部放在一起,效果很好。流试图模仿tail -f命令在你的终端上的行为。如果你从来没有见过这个命令,那是一个*nix命令,它显示一个文件的最后一行,并保持监听该文件的变化,所以当你添加一个新行时,它会立即列出它。同样的情况发生在流上。它们非常强大,在正确的使用情况下非常有用。你可以在这里阅读更多关于它们的信息。
所有这些功能都允许你以这种或那种方式与你的进程进行交流,根据你所追求的行为类型,你可能想解决其中的一个或所有的问题。
让我们快速看一下一些例子,让你了解什么时候使用。
基于事件的经典信息传递
最简单的例子是,每一个微服务都在等待一些事情发生。一个事件被触发,而这个事件可能来自外部,即系统的用户或客户端。
看一下上图,考虑中央的红色管子是Redis的Pub/Sub或Blocking list,它是一个更可靠的Pub/Sub的定制实现。
流程从1号开始,由 "客户应用 "提交请求,在9号结束,由 "客户应用 "得到响应通知。其他的呢?我不关心,客户应用也不应该关心。
这就是这种模式的好处之一,架构对客户来说成为一个黑盒子。一个请求可以触发数百个事件,也可以只触发一个,行为都是一样的:一旦响应准备好了,就会交付给客户端。而不是让客户端知道需要多长时间或者需要多久检查一次是否准备好。这些在这里都不重要。
请记住以下的注意事项。
- 一条消息由其订阅者发布到一个 "频道 "中。如果你想发布不同类型的主题,建议你有不同的频道。另外,如果你需要额外的粒度来区分哪个消费者需要处理某个特定的消息,那么细节就需要成为消息的一部分。这是因为一个通道的所有订阅者都会得到相同的消息,所以如果你有多个进程监听并得到相同的消息,你可能最终会重新采取相同的行动。在Redis中可以用消息的ID实现一个标志(例如),以确保第一个创建该消息的进程将负责处理该事件,而其他进程可以忽略它。这是一个可靠的方法,因为在Redis中设置一个键是一个原子过程,所以并发性不会影响到这个因素。
- 如果没有订阅者监听某个特定的通道,发布的消息就会丢失。如果你使用Pub/Sub模式,就会出现这种情况,因为它在 "fire & forget "机制下工作。如果你想确保你的信息在被处理之前一直在那里,你可以采用 "阻止列表 "的方法。这种解决方案包括直接在Redis的键空间上创建一个列表(即一个正常的值列表),并让进程订阅以获得围绕该键的键空间通知。这样他们就可以决定如何处理插入的数据(即如果他们想忽略它,处理它并删除它,等等)。
- 如果你要发送一个复杂的消息,如JSON,它需要被序列化。这是因为对于阻塞列表和Pub/Sub来说,你能发送的唯一东西是一个字符串。也就是说,如果你需要通过电线发送复杂的类型而不需要序列化,你可以考虑使用Redis Streams,这是他们所允许的。当然,这里的限制是,唯一允许的类型是Redis的,而不是你用来编码解决方案的语言的类型。
现在让我们来看看如果你的事件触发依赖于某些时间会发生什么。
基于时间的触发
反应式架构的另一个常见行为,是能够在预先定义的时间过后触发某些事件。例如:在发现数据有问题的10分钟后触发警报。或者等待30分钟后,触发一个物联网设备停止发送数据的警报。
这些通常是与现实世界的限制有关的行为,这些限制需要一点时间来解决,或者甚至可以通过 "等待一下 "和重新启动倒计时来解决,以备不时之需(如物联网设备有一个不可靠的连接)。
对于这种情况,架构保持不变,唯一的区别是中央通信枢纽肯定使用Redis的钥匙空间通知。
你看,有两个关于Redis的主要特点,你需要了解一下才能做到这一点。
- 当你设置一个键值对时,你可以选择性地定义一个TTL(生存时间),单位是秒。这就变成了一个倒计时,一旦达到0,这个键就会自动销毁。
- 当你订阅一个钥匙空间时(这也适用于pub/sub,但我们在这里不使用),你可以使用一个模式进行订阅。换句话说,你可以订阅 "last_connection_time_of_device100002 "这个键的事件,而不是订阅 "last_connection_time_of_device*"。然后,每一个创建的、符合该模式的键都会在它发生变化时通知你。
考虑到这两点,你可以创建订阅这些特定键的服务,一旦它们被移除(即当事件被触发时),你就会做出反应。同时,你让生产者不断地更新这些键,这也会重置TTL计时器。因此,如果你要追踪一个设备最后一次发送心跳的时间,你可以像我上面展示的那样,为每个设备设置一个密钥,并在每次收到新的心跳时持续更新该密钥。一旦TTL过了,就意味着你在配置的时间内没有收到新的心跳。你的订阅进程只会收到密钥名称,所以如果你只需要设备的ID,你可以像我展示的那样构造你的密钥,并解析名称以捕获所需的信息。
影子钥匙技术
另一方面,如果你在该钥匙内保存了一个复杂的结构,并且你需要它,你就必须改变一下这种方法。这是因为当TTL过期时,密钥就会被删除,因此里面的数据也会被删除,所以你无法真正检索到它。这时,你可以使用一种叫做 "影子密钥 "的技术。
影子钥匙,本质上是一个用来触发事件的钥匙,但它实际上是对包含你需要的数据的实际钥匙的影子。所以回到我们的例子,考虑生产者每次收到心跳时都会更新2个键。
- "last_connection_time_of_device100002 "是最后一次从设备收到有效载荷的unix时间戳。
- "device_data_id100002 "带有设备的额外信息。
在这两个键中,只有第一个键会有TTL,第二个键没有。因此,当你收到过期通知时,你将从过期的密钥中获取ID(last_connection_time_of_device100002),并使用它来读取第二个密钥的内容。然后,如果你需要的话,你也可以继续删除这个其他的密钥,或者把它留在那里,不管你的使用情况如何。
这里唯一需要考虑的是,如果你把Redis配置为集群模式,钥匙空间的通知不会传播到整个集群。这意味着,你必须确保你的消费者连接到每个节点。有些通知会因为没有人接收而丢失。这是这项技术的唯一缺点,但在你花数天时间调试你的异步逻辑之前,了解它是很重要的(去过那里,做过那里)。
正如你所看到的,这两种情况下的复杂性被降低到只是确保你订阅到正确的事件或分发渠道。如果你试图用一个普通的SQL数据库来做这件事,你必须围绕你的代码创建大量的逻辑,以有效地确定什么时候有新的数据进入,或者什么时候有一条信息被删除。
相反,这里所有的复杂性都被Redis抽象化了,你所需要担心的就是编写你的业务逻辑。对我来说,这就是黄金。
你是否将Redis用于缓存之外的任何其他场景?请在评论中与其他人分享你的经验,我很想知道你们是如何使用我最喜欢的技术之一的。
用Bit构建和分享独立的JS组件
比特是一个可扩展的工具,可以让你用_独立_编写、版本和维护的组件创建_真正的模块化_应用 。
用它来构建模块化的应用程序和设计系统,编写和交付微型前端,或者简单地在应用程序之间共享组件。
一个独立的源码控制和共享的 "卡片 "组件(右侧→其依赖关系图,由Bit自动生成)。
了解更多
围绕Redis构建一个反应式架构》最初发表于Medium上的Bits and Pieces,人们在这里通过强调和回应这个故事来继续对话。