dot Redis 8 已发布——它是开源的

了解更多

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

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

根据我的经验,将某些应用程序分解成更小、松散耦合、自包含的逻辑业务服务单元,使其协同工作,可以使构建和维护变得更容易。这些服务(也称为微服务)每个都管理自己的技术栈,可以独立于其他服务轻松开发和部署。使用这种架构设计有无数已被充分记录的好处,其他人已详细阐述。话虽如此,这种设计有一个方面我总是非常关注,因为当我没有注意时,它导致了一些有趣的挑战。

虽然构建松散耦合的微服务是一个极其轻量且快速的开发过程,但服务间通信模型(用于在这些服务之间共享状态、事件和数据)并不那么简单。我使用过的最简单的通信模型是直接的服务间通信。然而,正如 Fernando Dogio 在文章中雄辩地解释的那样,它在大规模应用中会失效——导致服务崩溃、重试逻辑以及负载增加时带来的严重麻烦——应不惜一切代价避免。其他通信模型包括通用的 Pub/Sub 到复杂的 Kafka 事件流,但最近我一直使用 Redis 来实现微服务间的通信。

Redis 来救援!

微服务通过网络边界分布状态。为了跟踪这些状态,事件应该存储在一个事件存储中。由于这些事件通常是异步写操作(也称为事务日志)的不可变记录流,因此具备以下属性:

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

使用 Redis,我一直都能轻松实现发布/订阅模式。但现在随着 Redis 5.0 推出了新的 Stream 数据类型,我们可以用更抽象的方式建模日志数据结构——使其成为时间序列数据(如具备至多一次或至少一次传递语义的事务日志)的理想用例。凭借 Active-Active 能力、简单易用的部署以及内存中的超快速处理,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 个随机客户Set

 

Stream

Hash

customer_ids

 

events:customer_created

customer_entity:customer_id

test_2_create_products创建 10 个随机产品名称Set

 

Stream

Hash

product_ids

 

events:product_created

product_entity:product_id

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

 

Stream

Hash

inventory_ids

 

events:inventory_created

inventory_entity:inventory_id

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

 

Stream

Hash

order_ids

 

events:order_created

order_product_ids:<>

test_5_update_second_order更新第二个订单Streamevents:order_updated
test_6_delete_third_order删除第三个订单Streamevents:order_deleted
test_7_delete_third_customer删除第三个客户Streamevents:customer_deleted
test_8_perform_billing对第一个订单执行计费Set

 

Stream

Hash

billing_ids

 

events:billing_created

billing_entity:billing_id

test_9_get_unbilled_orders获取未计费订单Set

 

Hash

billing_ids, order_ids

 

billing_entity:billing_id, order_entity:order_id

我选择使用 Streams 数据类型来保存这些 事件,因为其背后的抽象数据类型是事务日志,这完全符合我们持续事件流的用例。我选择不同的键来分配分区,并决定 为每个流生成自己的条目 ID,该 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 (UUIDs),以及 Lists 和 Hashes 来建模数据,因为 这反映了它们的结构,并且 实体缓存只是领域模型的一个简单投影。

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) “[email protected]

结论

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

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

如有任何问题 或想分享您的反馈,请随时与我联系。

再见