dot 快速的未来正在您所在的城市发生。

加入我们在 Redis 发布

如何使用 Redis 作为微服务间通信的事件存储

点击此处开始使用 Redis Enterprise。Redis Enterprise 允许您以任何规模、在任何地方处理任何实时数据。

根据我的经验,某些应用程序在分解成更小、松散耦合、自包含的逻辑业务服务块并协同工作时,更易于构建和维护。这些服务中的每一个(又称微服务)都管理着自己的技术栈,该技术栈易于独立于其他服务进行开发和部署。这种架构设计有很多经过充分记录的好处,其他人已经对此进行了详细的阐述。也就是说,这种设计有一个方面我一直非常注意,因为当我没有注意时,会导致一些有趣的挑战。

虽然构建松散耦合的微服务是一个非常轻量级且快速开发的过程,但这些服务之间共享状态、事件和数据的服务间通信模型并不那么简单。我使用过的最简单的通信模型是直接服务间通信。但是,正如 Fernando Dogio 在 文章 中精辟地解释的那样,它在规模上会失败——导致服务崩溃、重试逻辑和负载增加时的严重头痛——应该不惜一切代价避免。其他通信模型从通用发布/订阅到复杂的 Kafka 事件流,但我最近一直在使用 Redis 进行微服务之间的通信。

Redis 来拯救!

微服务在网络边界上分配状态。为了跟踪这种状态,事件应该存储在,比如,一个事件存储中。由于这些事件通常是异步写入操作(又称事务日志)的不可变记录流,因此适用以下属性

  1. 顺序很重要(时间序列数据)
  2. 丢失一个事件会导致错误的状态
  3. 在任何给定时间点都知道重播状态
  4. 写入操作简单快捷
  5. 读取操作需要更多努力,因此应该被缓存
  6. 需要高可扩展性,因为每个服务都是解耦的,彼此不知道

使用 Redis,我一直很容易实现 发布/订阅 模式。但现在新的 Stream 数据类型可与 Redis 5.0 一起使用,我们可以以更抽象的方式对日志数据结构进行建模——使其成为时间序列数据(如带有至多一次或至少一次传递语义的事务日志)的理想用例。再加上主动-主动功能、简单易用的部署和内存中超快的处理能力,Redis Streams 是大规模管理微服务通信必不可少的工具。

基本模式称为命令查询责任隔离 (CQRS)。它将命令和查询的执行方式分开。在这种情况下,命令通过 HTTP 执行,查询通过 RESP(Redis 序列化协议)执行。

让我们使用一个示例来演示如何使用 Redis 创建事件存储。

OrderShop 示例应用程序概述

我为一个简单但常见的电子商务用例创建了一个应用程序。当创建/删除客户、库存商品或订单时,应该使用 RESP 将事件异步通知 CRM 服务,以管理 OrderShop 与现有客户和潜在客户的互动。与许多常见的应用程序需求一样,CRM 服务可以在运行时启动和停止,而不会影响其他微服务。这需要在 CRM 服务停机期间发送给它的所有消息都必须被捕获以供处理。

下图显示了九个解耦微服务的互连性,它们使用使用 Redis Streams 构建的事件存储进行服务间通信。它们通过监听事件存储中特定事件流上新创建的任何事件来做到这一点,即 Redis 实例。

OrderShop Architecture

图 1:OrderShop 架构

我们 OrderShop 应用程序的域模型包含以下五个实体

  1. 客户
  2. 产品
  3. 库存
  4. 订单
  5. 结算

通过监听域事件并将实体缓存更新到最新状态,事件存储的聚合函数只需要调用一次或在回复时调用。

OrderShop Domain Model

图 2:OrderShop 域模型

安装并运行 OrderShop

要自己试用

  1. https://github.com/Redislabs-Solution-Architects/ordershop 克隆存储库
  2. 确保你已经安装了 Docker EngineDocker Compose
  3. 安装 Python3 (https://python-docs.readthedocs.io/en/latest/starting/install3/osx.html)
  4. 使用 docker-compose up 启动应用程序
  5. 使用 pip3 install -r client/requirements.txt 安装需求
  6. 然后使用 python3 -m unittest client/client.py 执行客户端
  7. 使用 docker-compose stop crm-service 停止 CRM 服务
  8. 重新执行客户端,您将看到应用程序在没有任何错误的情况下运行

幕后

以下是 client.py 中的一些示例测试用例,以及相应的 Redis 数据类型和键。

测试用例描述类型
test_1_create_customers创建 10 个随机客户集合

 

散列

customer_ids

 

events:customer_created

customer_entity:customer_id

test_2_create_products创建 10 个随机产品名称集合

 

散列

product_ids

 

events:product_created

product_entity:product_id

test_3_create_inventory为所有产品创建 100 个库存集合

 

散列

inventory_ids

 

events:inventory_created

inventory_entity:inventory_id

test_4_create_orders为所有客户创建 10 个订单集合

 

散列

order_ids

 

events:order_created

order_product_ids:<>

test_5_update_second_order更新第二个订单events:order_updated
test_6_delete_third_order删除第三个订单events:order_deleted
test_7_delete_third_customer删除第三个客户events:customer_deleted
test_8_perform_billing执行第一个订单的结算集合

 

散列

billing_ids

 

events:billing_created

billing_entity:billing_id

test_9_get_unbilled_orders获取未结算的订单集合

 

散列

billing_ids, order_ids

 

billing_entity:billing_id, order_entity:order_id

我选择 Streams 数据类型来保存这些事件,因为它们背后的抽象数据类型是事务日志,这完美地符合我们对连续事件流的用例。我选择不同的键来分配分区,并决定为每个流生成自己的条目 ID,它由以秒为单位的时间戳“-”微秒组成(以确保唯一性并保留跨键/分区的事件顺序)。

127.0.0.1:6379> XINFO STREAM events:order_created
 1) “length”
 2) (integer) 10
 3) “radix-tree-keys”
 4) (integer) 1
 5) “radix-tree-nodes”
 6) (integer) 2
 7) “groups”
 8) (integer) 0
 9) “last-generated-id”
10) “1548699679211-658”
11) “first-entry”
12) 1) “1548699678802-91”
    2) 1) “event_id”
       2) “fdd528d9-d469-42c1-be95-8ce2b2edbd63”
       3) “entity”
       4) “{\”id\”: \”b7663295-b973-42dc-b7bf-8e488e829d10\”, \”product_ids\”:
[\”7380449c-d4ed-41b8-9b6d-73805b944939\”, \”d3c32e76-c175-4037-ade3-ec6b76c8045d\”,
\”7380449c-d4ed-41b8-9b6d-73805b944939\”, \”93be6597-19d2-464e-882a-e4920154ba0e\”,
\”2093893d-53e9-4d97-bbf8-8a943ba5afde\”, \”7380449c-d4ed-41b8-9b6d-73805b944939\”],
\”customer_id\”: \”63a95f27-42c5-4aa8-9e40-1b59b0626756\”}”
13) “last-entry”
14) 1) “1548699679211-658”
    2) 1) “event_id”
       2) “164f9f4e-bfd7-4aaf-8717-70fc0c7b3647”
       3) “entity”
       4) “{\”id\”: \”1ea7f394-e9e9-4b02-8c29-547f8bcd2dde\”, \”product_ids\”:
[\”2093893d-53e9-4d97-bbf8-8a943ba5afde\”], \”customer_id\”:
\”8e8471c7-2f48-4e45-87ac-3c840cb63e60\”}”

我选择 Sets 来存储 ID(UUID)以及 ListsHashes 来对数据进行建模,因为它反映了它们的结构,并且实体缓存只是域模型的简单投影。

127.0.0.1:6379> TYPE customer_ids
set

127.0.0.1:6379> SMEMBERS customer_ids
1) “3b1c09fa-2feb-4c73-9e85-06131ec2548f”
2) “47c33e78-5e50-4f0f-8048-dd33efff777e”
3) “8bedc5f3-98f0-4623-8aba-4a477c1dd1d2”
4) “5f12bda4-be4d-48d4-bc42-e9d9d37881ed”
5) “aceb5838-e21b-4cc3-b59c-aefae5389335”
6) “63a95f27-42c5-4aa8-9e40-1b59b0626756”
7) “8e8471c7-2f48-4e45-87ac-3c840cb63e60”
8) “fe897703-826b-49ba-b000-27ba5da20505”
9) “67ded96e-a4b4-404e-ace6-3b8f4dea4038”

127.0.0.1:6379> TYPE customer_entity:67ded96e-a4b4-404e-ace6-3b8f4dea4038
hash

127.0.0.1:6379> HVALS customer_entity:67ded96e-a4b4-404e-ace6-3b8f4dea4038
1) “67ded96e-a4b4-404e-ace6-3b8f4dea4038”
2) “Ximnezmdmb”
3) “ximnezmdmb@server.com”

结论

Redis 提供的各种数据结构——包括 Sets、Sorted Sets、Hashes、Lists、Strings、Bit Arrays、HyperLogLogs、Geospatial Indexes 和现在的 Streams——可以轻松地适应任何数据模型。Streams 具有不止一个字符串的元素,而是由字段和值组成的对象。范围查询速度很快,并且流中的每个条目都有一个 ID,它是一个逻辑偏移量。Streams 为时间序列等用例提供了解决方案,以及为其他用例(如替换需要比即发即忘更可靠的通用发布/订阅应用程序以及完全新的用例)提供流式消息。

由于您可以通过分片(通过将多个实例集群)扩展 Redis 实例并提供用于灾难恢复的持久性选项,因此 Redis 是一个企业级选择。

如有任何问题或反馈意见,请随时与我联系。

再见