重新认识事件驱动架构

架构师成长系列

Posted by 薛以致用 on June 27, 2020

事件驱动的架构设计很多人都非常熟悉,尤其是 SOA 和微服务架构的广泛流行,但现实中要实现一个真正的事件驱动的应用并不是一个简单的事情,甚至不同开发者在一起探讨的“事件”是完全不同的概念。

用意图区分消息、事件和命令

Message vs Event vs Command

事件驱动的场景里面,我们会不断听到 “消息”、”事件“和”命令“,因此,我们首先要达成一致的理解,如何区别这三者?一个比较好的实践是通过 ”意图“ 来区分,如上图所示,所谓 ”消息“ 是”事件“和”命令“的泛化,没有特别明确的意图;“命令”就非常具体,通常是发生在生产者(发出命令的一方)和消费者(接受和执行命令的一方)1对1的通信,表示生产者期望消费者有所行动,命令必须能够被消费者接受到;而 “事件”消息,一个生产者产生的事件,可以被多个消费者接受,生产者不关注消费者是否或如何响应事件,事件必须被生产者发布。

事务消息(Transactional Message)

微服务加上消息中间件,在实现分布式最终一致性的问题上,业务逻辑通常需要参与来处理本地数据库事务和消息中间件事务的原子性问题;比如订单花费100元,会员系统增加100个积分问题,订单系统涉及到先写本地数据库(订单成功)再发布事件(通知会员系统),还是先发布事件再写本地数据库?无论哪种情况都会出现,一个操作成功,但另外一个操作失败的情况,这时就会导致系统不一致状况。

如果没有事务消息,那生产者就需要自己管理事件发布的状态信息,已确保本地订单写数据库成功的同时,事件也发布成功;

RocketMQ 的事务消息解决的是本地事务执行与消息发送的原子性问题。即解决生成者执行业务逻辑成功之后投递消息可能失败的场景。RocketMQ 事务消息的设计流程借鉴了两阶段提交原理,通过在执行本地事务前后发送两条消息来保证本地事务与发送消息的原子性,过程见下图:

CQRS

“事件风暴” Event Storming

Event Storming 结果

熟悉领域驱动设计的人通常都比较熟悉事件风暴,但事件风暴不完全等同于领域驱动设计,和 Design Thinking 互动方法类似,它倡导敞开门办公(开放空间),通过交互式白板互动,和所有拥有业务领域知识的伙伴一起梳理用户旅程,同时它聚焦在用户旅程中发生的所有“领域事件”。

因此参与者不需要特别的培训,只需要投入时间参与互动,利用白板笔和不同颜色的便利贴即可,下面是简单的“游戏”步骤:

  • 找出领域事件

领域事件,是跟业务流程关联的特别重要的事件,反应了业务领域对象状态变化,用名称+动词过去式来表达,比如 OrderSubmitted,PaymentProcessed,等等;

  • 排序领域事件

不同业务的用户旅程是不一样的,所以,根据实际领域事件发生顺序进行排序。

  • 识别产生领域事件的“用户”和命令

这个阶段一起探讨,是什么触发了事件?是一个用户,另外一个系统,还是另外一个事件?触发事件的命令是什么?

  • 事件归类(聚合)

把领域事件,按照所作用的对象进行简单归类

  • 进一步归类(限界上下文)

主要是产生高内聚的业务实体(为高内聚的领域服务实现做准备),避免跨领域边界上下文的依赖。

Event Storming - Sticky

更多的关于事件风暴的内容,请参考文末扩展阅读资料。

常见的事件驱动范式

从事件风暴可见,围绕领域事件进行互动探讨的方式,对于梳理用户旅程,分解和归纳业务流程有很强的实践性,同时侧面也验证了“事件”在业务到系统实现的重要性。事件和命令相辅相成,适合不同的场景,对应到微服务领域,如下图所示,命令通常的表现形式是 API 接口调用,服务 A 调用 服务 B就产生了一定的依赖关系;而事件驱动架构通常利用独立的持久化存储来解耦不同服务关联。

Event Driven Patterns

下面我们复习下 Martin Fowler 2017年关于事件驱动架构的思考和分享:

事件通知模式 (Event Notification)

该模式下,事件的生产者不关心事件后续的反馈;事件通知模式使用较小的消息来指示某些状态更新(例如新订单或更新邮寄地址)。

与其他分布式事件模式一样,事件可以通过订阅源提供给消费者,或者可以使用消息中间件(如消息代理)。

事件通知模式好处是简单,容易实现解耦的服务架构;但如果存在一个业务逻辑依赖于多个事件通知,该模式下会带来挑战,因为很难调试整个业务逻辑,必须实时监控整个系统;

该模式下的事件,不需要携带太多的数据,通常一个 ID 信息和一个访问事件生产者的链接信息,用来查询更丰富的事件信息;消费者了解某些事情发生了,从事件本身了解非常有限的信息,根据需要调用生产者接口查询更多信息。

  "event" : "order_changed",
  "order_id" : "qhqlvvx6lwf9",
  "href" : "http://sales/order/qhqlvvx6lwf9"
}

上面例子的一个假设是,事件所对应的实体总是包含最新的状态信息,此时由事件生产者维护所有的状态信息,消费者只能了解到最新的状态,事件中没有指示对订单所做的更改。反向引用指向实体资源本身。通知的简洁性是该实现的一个典型特征;消费者代码没有什么工作,按需要读取订单资源的接口继续工作。

与事件关联到最新状态实体对象不一样的是,有些消费者期望了解触发该事件时的实体对象数据,就需要事件生产者提供版本信息,比如 http://some.domain/customer/{customer-id}/2 是指客户对象的第二个版本;另外,还有些消费者关注事件发生前后的实体差异,在版本的基础上,可以提供类似 http://sales/order/{order-id}/2/3 的引用,如此,消费者就可以了解版本 2和 版本3 之间的订单数据差异。

该模式下,通常要求事件生产者要一直在线,否则,事件消费者虽然感知到事件发生,但无法了解具体生产者实体对象的具体情况而无法继续后续任务

Event Notification

事件传递状态转移 (Event Carried State Transfer)

该模式下,生产者把详细状态数据添加到事件中,如此对消费者而言,不需要再访问事件生产者服务了解详情就可以完成后续任务。事件生产者需要仔细衡量,提供足够的信息以便对已知和未知订阅者有用。

比如一个 CMS(Customer Management System)系统发布了某个用户地址更新事件,该事件包含详细的用户地址信息,某个消费者服务可以直接订阅该事件,并相应的使用事件中的用户地址信息更新到自身的数据库中,这样消费者不需要再访问 CMS 系统就可以完成这样的更新。

该模式的好处显而易见,通过数据冗余增加了系统的可恢复性能力,因为消费者不依赖生产者就可以独立提供服务,同时可以降低延迟,不需要远程调用生产者系统,也不用担心生产者系统来自不同消费者过多的查询工作负载;但副作用也很明显,大量的副本,带来冗余的存储使用,同时增加了消费者从所有事件中维护一致性状态的复杂性(需要依赖事件顺序)。

解耦发布者和订阅者是事件驱动架构的一个非常好的优势;但是,这种解耦架构也带来了很大的整体一致性复杂度。

事件通知模式可以通过快速处理事件来解决系统不一致性挑战;不同系统组件在一段时间内仍然不一致,直到事件处理完毕,但由于订阅者可以回调生产者服务,它会看到当前状态,系统不需要关注事件竞争条件和事件排序。

事件溯源(Event Sourcing)

事件溯源一个最直观的例子就是版本控制系统(git),所有的提交(Commit)都是事件记录,包含变更数据,当前复制的代码库副本是系统状态。也就是说,每当我们对系统状态进行变更时,将状态更改记录成事件,所有的事件存储作为唯一的事实来源(Source Of Turth),未来任何时间点,我们可以非常有信心重新处理事件来重建系统状态。

Event Sourcing

见上图,我们想象一个最简单的电子投票系统的例子,投票系统本质上是写入密集型的系统:计数最终结果才重要,但人们一直提交投票。因此,如果使用关系 (SQL) 数据库作为数据库后端,它可能很容易被压垮,因为每次投票都需要更新一个表并锁定行然后释放。但是,对于事件溯源,接受投票只需要在事件日志中插入:由于每个投票(事件)都是不可变的,因此不需要锁定。当你需要最终计数时,简单地读取记录的事件列表并累加投票计数即可:

使用事件日志时,构建工作副本的快照通常很有用,这样您就不必每次需要工作副本时从头开始处理所有事件。

事件溯源有许多有趣的好处,在考虑版本控制系统的价值时,很容易想到这些好处。事件日志提供强大的审计功能。我们可以通过将事件日志重放到某个点来重新创建历史状态。我们可以通过在重放时注入假设事件来探索替代历史。事件溯源使得拥有不持久的工作副本(如内存映像)是合理的。

事件溯源确实存在其问题。当业务结果取决于与外部系统的交互时,重放事件会成为问题。我们必须弄清楚如何处理随着时间的推移,事件数据结构的变化。

命令查询分离 CQRS

Command Query Responsibility Segregation (CQRS) 严格来说并不是真正的事件驱动模式,因为你可以使用 CQRS 而不存在任何事件在架构设计中;但大家通常把 CQRS 与早期的事件驱动模式放在一起;

CQRS 的理念是,在复杂的业务领域中,处理读取和写入的单一模型过于复杂,我们可以通过分离关注点来简化。当您在访问模式方面存在差异(例如大量读取和很少写入)时,这尤其有吸引力。但是,使用 CQRS 的优势必须与具有单独读写模型的额外复杂性进行平衡。

CQRS

参考资料和扩展阅读:

  • GOTO 2017年•《事件驱动架构的多种含义•马丁·福勒》 https://www.youtube.com/watch?v=STKCRSUsyP0&feature=youtu.be
  • 《Microservices — When to React Vs. Orchestrate》https://medium.com/capital-one-tech/microservices-when-to-react-vs-orchestrate-c6b18308a14c
  • 《Decomposing the Monolith with Event Storming》https://medium.com/capital-one-tech/event-storming-decomposing-the-monolith-to-kick-start-your-microservice-architecture-acb8695a6e61
  • 《What do you mean by “Event-Driven”?》 https://martinfowler.com/articles/201701-event-driven.html
  • 《The Design Of Transactional Message》http://rocketmq.apache.org/rocketmq/the-design-of-transactional-message/

申明

本站点所有文章,仅代表个人想法,不代表任何公司立场,所有数据都来自公开资料

转载请注明出处


公众号二维码

诞生于 2019,遇见 2020。

感谢关注,欢迎动动手指标星和置顶;

这样就不会错过少但精彩的技术探讨、团队建设、案例分享!

每周至少一更,转发是对我的最大鼓励!

学习之路漫漫,走走停停,
偶有所感,随心所记,
言由心声,问心无愧!

从客户中来,到客户中去!