基于事件驱动架构的一些设计难题及解决方法

Posted by 皮皮潘 on 01-06,2022

处理并发和消息顺序

问题描述

在保留消息顺序的同时横向扩展多个接收方的实例往往会产生并发和消息顺序的问题,例如:假设有3个相同的接收方实例从同一个点对点通道读取消息,发送方按顺序发布了Order Created、Order Updated和Order Cancelled这3个事件消息,如果简单地将每个消息负载均衡地发送给不同的接收方,有网络问题等原因导致的延时可能会使得A实例在处理Order Created之前B实例已经在处理Order Cancelled消息了。

解决方案

现代消息代理,如:Kafka,Kinesis使用的常见解决方案是使用分区通道,该解决方案分为三个部分:

  1. 分区通道由多个分区组成,每个分区的行为类似一个通道
  2. 发送方在消息头部指定分区键,消息代理使用分区键将消息分配给特定的分区(一般计算对应的Hash值)
  3. 消息代理将每个分区分配给单个接收器实例,它在接收方启动和关闭时重新分配分区

在之前的问题描述中,通过该解决方案,以OrderId作为分区键就能保证同一个Order的事件都顺序地被同一个实例处理

处理重复消息

问题描述

理想情况下,消息代理应该只传递一次消息,但是保证有且仅有一次的代价很高,因此大多数消息代理往往承诺至少成功传递一次消息,当消息多次传递也即发生重复消息时需要有对应的解决方案

另外消息代理需要在重新传递消息时保留排序,也即当第一个消息丢失但是第二个消息收到时,在重发了第一个消息以后也要重发第二个消息,可以考虑第一个消息是Order Cancelled,第二个消息是Order Created的情况,如果没有重发第二个消息,会导致重发的第一个消息覆盖原来的Order Created的消息造成逻辑错误,在这种情况下也会发生重复消息

解决方案

编写幂等消息处理器

如果应用程序处理消息的逻辑是满足幂等的,那么重复的消息就是无害的,不幸的是,应用程序逻辑通常不是幂等的,在这种情况下需要编写跟踪消息并丢弃重复消息的消息处理程序

跟踪消息并丢弃重复消息

使用message id跟踪已处理的消息并丢弃任何重复,例如:在数据库表中存储它消费的每条消息的message id,同时在处理消息时,将消息的message id作为创建和更新业务实体的事务的一部分记录数据库表中,此时重复的message id会导致事务失败以及回滚

事务性消息

问题描述

服务通常需要在更新数据库的事务中发布消息,在这里需要保证消息发布与数据库更新操作的原子性,如果服务不以原子方式执行这两个操作,则类似的故障会导致系统处于不一致的状态

解决方案

在之前的分布式事务原理博客中的可靠消息最终一致性分布式事务一章讲解了对应的解决方案,这里不再赘述