微服务架构下BC(限界上下文)往往被独立部署成微服务,一个完整业务流程经常需要多个BC共同完成。因此架构师的视角不仅仅要关注BC内部的架构(领域建模、分层架构、编码规范等),还要关注BC之间的架构。首当其冲的就是BC间通过领域事件进行异步通信的架构。

以营销场景的线索业务举例,线索服务对接着广告系统,广告系统会触发线索生成,然后触发线索清洗打标签、落潜通知等业务。微服务架构下落潜业务涉及到三个独立微服务。在不引入事件的情况下,三个服务通过API进行通信,如下图所示。

图片

02

分布式事务问题

同步通信要解决分布式事务问题,即如何保证三个步骤都成功执行或者都不执行。步骤#1虽然有数据库事务保证,但步骤#2和#3涉及跨进程网络调用,有可能出现第三态即超时(意味着可能是成功也可能是失败)。业界处理分布式事务往往是有两种方案。

强一致性方案

如果要保证严格的类似数据事务那种ACID都满足的强一致性,需要引入分布式事务框架。

  • 数据库侧自身的分布式事务方案,如Spanner、OceanBase

  • 应用程序侧结合TCC、共识算法等框架

强一致性不是本文重点,细节略过。

弱一致性方案(最终一致性)

实际业务当中绝大多数场景并不需要强一致性。这样API同步通信就可以转为消息方式的异步通信,接下来的问题就是本地数据保存和消息投递的一致性事务问题,业界的解决方案大致有两种,其中第二种又有两种细分。

  • 通过消息队列中间件本身的事务消息,如RocketMQ

  • 基于发件箱模式的本地事件表

  • 通过消息中继服务投递消息

  • 通过CDC技术投递消息

如果说消息方式属于推模式,有些场景下也可以采用拉模式。即下游服务主动去拉取上游服务的数据。但是不管是通过接口拉取还是直接访问数据库,都有拉模式的天然缺陷。拉取频率太低,数据不够实时。拉取频率太高,对源系统带来性能压力,影响源系统稳定。并且拉模式下整个业务链路不清晰,遇到系统异常不易修复链路上的相关数据。

03

事件驱动方案

方案1:MQ事务消息

MQ事务消息方案其实是由MQ自身实现了分布式事务,通过类似二阶段方案,引入了半消息机制和回查机制达到了应用程序的业务数据保存和消息投递的一致性状态。

图片

正常发送流程:

  1. 执行消息准备逻辑

  2. 发送事务消息

  3. 监听MQ的ack

  4. 执行业务逻辑并保存数据

  5. 给MQ ack确认状态

异常流程:

  1. MQ没收到确认信息,回查本地事务状态

  2. 查询本地数据

  3. 给MQ ack确认状态

方案的好处是只要按照MQ的要求完成相关接口的实现即可达到本地数据和消息发送的一致性。不好的地方是方案对应用程序的侵入性太大,业务逻辑需要按照要求拆分到不同接口里实现,图中蓝色部分是对接MQ的接口,绿色部分是业务方法。另外提供事务消息的MQ中间件并不多,移植性差,有被厂商锁定风险。

方案2:发件箱模式+消息中继

发件箱模式是为了降低对应用程序的侵入。其原理很简单,在一个数据库事务中既保存业务数据也保存一份消息记录。先做到业务处理成功后一定会有消息记录被创建。同时通过其他手段把消息投递到MQ里。

图片

负责投递消息的一般叫消息中继服务,它可以是独立的部署服务,也可以跟线索服务一起部署。它的关键设计需要考虑发布服务和消费服务的接入体验,自身的高可用和可观测(监控、告警)。确保消息能及时投递出去。中继服务在业界有一个开源方案是国外Groupon的killbill common queue。

方案3:发件箱模式+CDC

中继服务方案需要读取和更新消息表,这无疑会给业务服务操作业务数据表带来性能和稳定性的影响。因此为了进一步降低对业务服务数据库的影响,业界还有另一个方案。通过解析数据库的事务日志文件(比如MySQL的binlog,PostgreSQL的Write Ahead Log)组装成消息投递到消息队列,也叫变更数据捕获技术,即CDC技术(Change Data Capture)。

图片

CDC服务一般是独立部署,同样需要考虑高可用性和可观测。业界也有生产级开源实现,国内有阿里的Canal,国外有Redhat的Debezium。Canal主要支持MySQL,通过模拟成MySQL的slave得到master的binlog,把解析结果投递到多种目标源。Debezium是在Kafka的Connect基础上演变而来,支持多种数据库。另外各大公有云也有自研的DTS方案。

方案对比:中继服务方案更适合早期的中小规模服务集群,相比CDC的复杂度会简单些,但具有更高的侵入性,给业务服务带来一定的性能开销。

领域事件sdk关键实现

前面的方案2和3都需要考虑业务开发的便捷性,从发布到订阅的整个过程中如何最小程度侵入消息发布服务和订阅服务。可以从以下几个方面设计sdk:

发布方体验

封装事件

在DDD里领域事件是聚合维度的,因此每种领域事件除了包含事件的业务信息外,还必须提供所属的聚合名,每个事件实例需要提供聚合Id、事件Id,即实现getAggregateName()、getAggregateI()、getEventId()等领域事件接口

发送事件

发布服务组装好领域事件后,获取到发布类实例即通过调用publish方法发布出去。domainEventPublisher.publish(leadsCreatedEvent)

消费方体验

通过聚合和事件名过滤和路由,消费者服务只需要实现handle接口即可(当然需要支持幂等语义)。

@EventHandler(aggregateName = xxx, EventName = LeadsCreatedEvent.class)

public boolean handle(LeadsCreatedEvent leadsCreatedEvent){}

关键实现:

  • 实现DDD对领域事件的定义

  • 使用方感知不到消息表的存在

  • 使用方感知不到具体的MQ中间件

  • 支持单次和批次发送

  • 支持事件链路可观测(分布式链路ID植入和透传)

  • 支持租户隔离

  • 支持MQ灾备切换

04

领域事件正成为一等公民

高内聚低耦合是架构设计的终极目标,微服务架构下提倡微服务之间优先通过异步事件来实现松散耦合、高可扩展和高可用性。领域事件的使用场景也会越来越广泛。常见场景有:

  1. 用于推进正常的业务流程。当前业务节点处理完了,发布事件通知后续业务流程节点,这也是最常见的场景。

  2. 用于业务系统间的数据分发。源数据发生了变化,副本数据需要得到及时更新。比如读写分离场景下,线索业务数据存储在MySQL,线索查询的数据存储在ElasticSearch。

  3. 用于分析、审计、监控等场景下的业务数据复制、对账。

在领域事件驱动架构下,业务开发人员能围绕领域事件拆解业务流程,聚焦业务逻辑开发,最大程度减少了底层技术组件的干扰。业务服务跨云平台部署便捷。

领域事件驱动架构在云原生架构下会有更多可能,也可以像RPC同步通信那样通过mesh化进一步降低对业务服务的侵入。比如通过DAPR框架做到统一接管事件流量的路由和控制,进而释放更多全链路治理的可能性。