首页 > 解决方案 > 仅在 Aurora 事务提交时调用 Lambda 函数,但保证调用 (ACID)

问题描述

对于新项目中的微服务,我目前正在考虑是否使用 DynamoDB 或 Aurora MySQL 作为底层数据存储。微服务为用户界面提供了一个 REST API,并且还有其他几个微服务。这些其他微服务应该监听由 UI 连接服务生成的事件流(事件源),以保持其他读取模型同步。

我试图找出一种方法来保证发布到更改事件流的事件与底层数据存储中数据的更改完全匹配。通常,需要关注的是,如果 REST API 处理程序(例如)在其执行过程中被中断,它可能已经更改了数据但尚未创建事件(假设更改事件在数据更改之后发布)。我现在正在寻找能够缓解这种担忧的机制。

对于 DynamoDB,有 DynamoDB 流和 AWS Lambda 触发器来对数据存储级别的数据更改做出反应。触发的 Lambda 可以将低级数据更改转换为有意义的更改事件,然后将事件发布到 SNS、SQS 或 Kinesis。

对于 Aurora MySQL,我还没有想出这样的机制。我看过描述两种机制的文章:

  1. 为 Aurora 启用二进制日志并使用额外的 EC2 实例来处理更改流。从此流中发布其他服务的事件。
  2. 使用本机 lambda_sync 或 lambda_async 函数从 MySQL 触发器调用 Lambda。从此 Lambda 中发布其他服务的事件。

一,我对这两种方法都不太满意:1)我不希望管理额外的 EC2 实例并处理原始 SQL 更改。2)我打算对Aurora使用约束、乐观并发和事务,这意味着事务可以并且将会失败和回滚。但是,无论事务结果如何,都会执行 lambda_(a)sync 调用。

对 Aurora 有更好的想法吗?还是我从错误的角度看待这个问题?

我想将这个问题和讨论集中在如何保证基础数据存储的更改和具有更改事件的传出流之间的一致性,而不是关于 Aurora 与 DynamoDB 的问题。

标签: amazon-web-servicesmicroservicesamazon-aurora

解决方案


我找到了一个适合我们情况的答案,使用具有 MySQL 兼容性的 Aurora。在我的研究过程中,我在microservices.io发现了极好的信息来源。具体来说,关于事件驱动架构模式的页面参考了四种相关模式,以保证更新状态和发布事件的原子性。

  • 事件溯源
  • 应用事件
  • 数据库触发器
  • 事务日志拖尾

事件溯源是不可能的,因为它对于我们想要实现的目标来说太复杂了。我已经在我最初的问题中反对 tx log tailing。应用程序事件和数据库触发器非常相似,作为事务的一部分,状态被更新,并且条目被写入事件表:Tx 成功提交,状态被持久化并且事件条目显示在该表中。Tx 回滚,状态不变,没有事件条目出现。两者之间的唯一区别是事件条目是由应用程序/服务逻辑本身生成的,还是由数据库触发器生成的。

然后一个外部进程轮询这个表并根据事件条目发布其他微服务的事件(当然之后删除已发布的事件)。这两种模式保证状态更改总是会导致至少一个事件(恰好一次会更复杂一些)。

现在关于如何实现这个......我的第一个想法是使用 Fargate 容器和一个执行轮询的 Node 应用程序,我认为我会使用这个解决方案保持无服务器。然而,事实证明这并不完全正确:为了保证事件的顺序,应该只有一个容器轮询和发布。一个 Fargate 容器被分配给一个可用区域,如果该区域“消失”,容器也会“消失”。现在我必须在顶部构建某种监控,以便在需要时在不同的 AZ #2 中实例化一个新的容器 #2。但是如果 AZ #1 和容器 #1 回来怎么办?那么会有两种情况。这变得太复杂了。

现在我选择了以下方法:CloudWatch 事件每分钟触发一次轮询 Lambda 函数(CW 的最小间隔)。调用后,该函数会继续轮询数据库,直到一分钟后第二个 Lambda 函数调用接管。为了协调两个 Lambda 函数调用,我在我的数据库中创建了第二个表 Event Polling State,其中最近的 Lambda 函数调用更新了该表中的一个专用行,指示它开始的前一个函数调用(这个是在 SELECT ... FOR UPDATE 和 TX 的帮助下完成的,以防止出现竞争条件)。在每个轮询周期之前,该函数会检查处于事件轮询状态的行是否同时没有其他函数启动。

这种方法的优点(如我所见):

  • 真正的无服务器和 AZ-、VPC、子网不感知。
  • 事件的顺序得到保证,因为不会有超过一个 Lambda 调用并行轮询和发布。
  • 如果轮询 Lambda 函数因任何原因终止(例如因为 AZ 消失),则发布的间隔最多为 1 分钟,直到 CloudWatch 下次调用 Lambda 函数。这个差距是可以接受的。

推荐阅读