当前位置: 首页 > news >正文

Azure 设计模式之事件源模式

不应只存数据的当前状态,而要以追加的方式来存储每个操作。每个记录可用于实现域对象。这样就可以简化任务的复杂度,避免了数据模型和业务的同步,从而提高性能、可伸缩性和响应能力。它还可以提高事务数据一致性,并保存了完整审核跟踪和历史记录用于完成补偿操作。


问题背景
大多数应用程序都会用数据,而典型的方法是应用程序通过在用户处理数据时对其进行更新来维护其当前状态。例如,在传统的创建、读取、更新和删除(CRUD)模型中,典型的数据处理过程是从存储中读取数据,进行一些修改,并用新值更新数据的当前状态-通常是通过锁定数据的事务来完成。


CRUD 方法有一些局限性:
CRUD系统直接针对数据存储区执行更新操作,这会降低性能和响应能力,并由于所需的处理开销而限制了可伸缩性。在多并发用户的协作域中,数据更新冲突通常是因为更新操作发生在单个数据项上。除非在单独的日志中记录中,除了记录每个操作的详细信息,有单独的审核机制。否则历史记录将丢失。要深入了解crud的局限性, 请参阅:CRUD,仅当您能够负担得起时才用(https://blogs.msdn.microsoft.com/maarten_mullender/2004/07/23/crud-only-when-you-can-afford-it-revisited/)


解决方案
事件源模式定义了一种处理数据的方法,这些操作是由一系列事件驱动的,每个事件都记录在只追加的存储中。应用程序代码发送一系列事件,它们描述了从数据到事件存储区中所发生的每个操作,并将它们保存。每个事件表示对数据的一系列更改 (如 AddedItemToOrder)。


事件保存在事件存储区中作为系统记录,与数据的当前状态有关。事件存储区通常会发布这些事件,以便在需要时可以通知消费者进行处理。例如,使用者可以将事件中的操作应用到其他系统的任务中,或用于完成任何其他相关操作。请注意,生成事件的应用程序代码与订阅事件系统是分离(没有耦合)的。


事件存储发布事件的典型用法是维护实体的实例化视图,将其作为应用程序中的操作,进行更改或与外部系统进行集成。例如,可用于填充客户订单UI的实例化视图。当应用程序添加新订单或删除订单上的项目以及添加装运信息时,可以处理这些更改的事件,并用于更新实例化视图(https://docs.microsoft.com/en-us/azure/architecture/patterns/materialized-view)


此外,在任何时候,应用程序都可以读取事件的历史记录,并通过播放并调用与该实体相关的所有事件来还原实体的状态。可以用于当处理请求或使用计划任务处理域对象时,将实体的状态转换为实例化视图以支持表示层。


下图显示了该模式有关事件流一些使用场景,如创建实例化视图、将事件与外部应用程序和系统集成,以及事件重播以恢复当前状态。


事件源模式具有以下优点:
事件是不可变的,使用只追加操作进行存储。事件处理在后端线程进行,启动事件的用户界面、工作流或进程可以不被阻塞。如果结合在处理事务过程中无资源争用的事实,可极大地提高应用程序的性能和可伸缩性,尤其是在表示级别和用户UI层面。


事件是用于描述某操作的对象,以及该事件的任何相关数据。事件不直接更新数据存储。他们只是在适当的时间被处理。从而简化了实现和管理(复杂度)。


事件通常对领域专家具有意义,而对象-关系阻抗不匹配(https://en.wikipedia.org/wiki/Object-relational_impedance_mismatch) 使复杂的数据库表难以理解。表是人工构造,它仅表示系统的当前状态,而不是所发生的事件。


事件来源可以帮助防止并发更新导致的冲突,它避免了直接更新数据存储对象的需求。但是,在设计域模型时,仍需考虑如何免受可能导致不一致状态请求的攻击。事件的只追加存储提供了审核跟踪,可用于监视数据存储区的操作,在任何时间通过重播事件并将当前状态重新生成实例化视图,以协助测试和调试。此外,使用补偿事件来取消更改这一需求为已撤销的更改提供了历史记录,如果模型只是用来存当前状态,则不会用到这个功能。事件列表还可用于分析应用程序性能以及检测用户行为趋势,或获取其他业务信息。


存储事件用于任务来执行一些操作,并响应这些事件。从事件将任务分离可以提供灵活性和可扩展性。任务了解事件类型和数据,但不知道触发事件的操作。此外,多个任务可以同时处理事件。这使得与其他服务中的事件存储更易于集成。但是,事件源中事件的优先级往往很低,因此需要生成用于集成的特殊事件。


事件源通常与CQRS模式相结合,通过执行数据管理任务来响应事件,并将事件实例化为视图。


问题和注意事项
在决定如何实现此模式时,需要考虑以下几点:
在创建实例化视图或通过重播事件以生成数据投影时,系统最终将是一致的。将事件添加到事件存储区的应用程序与处理请求结果、正在发布的事件以及处理事件使用者之间存在一些延迟。在此期间,对实体进行更改的新事件可能已到达事件存储区。


注意
有关最终一致性的信息, 请参阅数据一致性入门(https://msdn.microsoft.com/library/dn589800.aspx)。


事件存储区是信息的永久来源,因此不应被更新。撤消更改的唯一方法是向事件存储区添加补偿事件。如果持久事件的格式(而不是数据)需要更改(可能是在数据迁移过程中),则很难将现有事件与新版本合并。可能需要循环访问所有发生更改的事件,使它们转换为新格式,再添加新格式的事件。可考虑在事件架构的每个版本上使用版本戳,以维护事件格式的历史记录。


多线程应用程序和多个应用程序实例可能会同时在事件存储区中存储事件。事件存储区中事件的一致性至关重要,就像事件顺序一样(对实体发生更改的顺序会影响其当前状态)。向每个事件添加时间戳可以帮助避免此问题。另一个常见的做法是用增量标识符对请求产生的每个事件进行注释。如果两操作同时为同一实体添加事件,则事件存储可以拒绝与现有实体标识符和事件标识符所匹配的事件。


对于获取事件信息的操作,没有标准的方法或现有的机制(如SQL查询)可以用。可提取的唯一数据是以使用事件标识符作为条件的事件流。事件ID通常映射到各个实体。实体的当前状态只能通过重播与该实体的原始状态相关的所有事件来确定。


每个事件流的长度会影响系统的管理和更新。如果流很大,考虑以特定的时间间隔(如指定的事件数)创建快照。可以从快照中获取实体的当前状态,并重放在该时间点之后发生的任何事件。有关创建数据快照的详细信息,请参阅马丁福勒的企业应用程序体系结构网站上的快照(http://martinfowler.com/eaaDev/Snapshot.html)和主从快照复制。(https://msdn.microsoft.com/library/ff650012.aspx)


尽管事件源最大限度地减少了数据更新发生冲突的可能性,但应用程序仍须能够处理最终一致性并缺少事务导致的不一致。例如,表示库存减少的事件可能会到达数据存储区,而同时生成了该物料的订单,因此需要通知客户或另创建一个订单来协调这两个操作。


事件发布的模式可能是"至少一次",因此对事件的操作必须是幂等的。也就是说如果事件多次被处理,则不能被重复更新。例如,如果使用者的多个实例引用了同一实体的属性(例如,订单总数),对这些实例而言,订单总数增加的操作只能发生一次。虽然这不是事件源的关键特性,但它是常见的实现决策。


何时使用此模式
在以下情况下考虑使用此模式:
当希望追踪数据中的意图、目的或原因时。例如,可以将对客户实体的更改映射为一系列特定的事件类型,如移动主页、关闭或注销帐户。希望最小化或完全避免数据更新产生的冲突。


如果要记录发生的事件,并能够重播它们以还原系统的状态,请回滚更改,或保留历史记录和审核日志。例如,当任务涉及多个步骤时,可能需要执行一些操作以恢复到某状态,然后重播某些步骤以使数据恢复到原始状态。


当应用程序已经使用了事件,引用事件源需要很少的额外开发工作。


当需要将输入或更新数据的过程与执行这些操作的任务分离时。这可能是为了提高UI性能,或者需要将事件分发给在事件的其他监听者。例如,将工资单系统与支出提交网站集成时,能够使事件存储下来为支出提交网站和工资系统共同使用。


如果希望能够灵活的更改实例化模型和实体数据的格式(如果需求发生变化),或者当与CQRS一起使用时需要调整读取模型或公开数据的视图。


如果与CQRS一起使用时,实体和事件流中数据交互带来的性能影响是可以接受的。


在下列情况下,此模式可能不会有用:
业务领域很简单,很少或根本没有业务逻辑的系统,它们能够与传统的CRUD数据管理机制很好地配合使用。


需要对数据视图进行一致性和实时更新的系统。


不需要审核跟踪、历史记录和回滚以及重播操作功能的系统。


系统中底层数据的更新发生冲突的次数非常少。例如,主要是添加数据而很少对其进行更新的系统。


例子
会议管理系统需要跟踪会议已完成预订的数量,以便可以在与会者尝试预订时检查是否仍有可用座位。系统可以至少以两种方式存储会议的预订总数:


系统可以将与预订总数相关的信息存储到预订信息的数据库中。当有新预订或订单取消时,系统会根据需要增加或减少数量。这种方法理论上很简单,但如果有大量与会者在短时间内预订座位时会导致可伸缩性问题。例如,在预订期结束前的最后一天。


系统可以将预订和取消的信息存储在事件存储区中。然后可以通过重播这些事件来计算可用的座位数。由于事件的不变性,此方法更具伸缩性。系统只需从事件存储区读取数据,将数据追加到事件存储区。有关预订和取消的事件的信息永远不会被修改。


下图说明了如何使用事件源来实现会议管理系统中的座位预留子系统。



保留两个席位的操作顺序如下:
用户界面发出命令,为两个与会者预留座位。该命令由单独的命令处理程序处理。与用户界面分离并负责处理命令的请求逻辑。


会议预留信息的合计操作是通过查询预订和取消事件来实现的。此聚合操作被称为SeatAvailability,包含在查询和修改聚合数据方法的域模型中。


要考虑的一些优化是使用快照(以便不需要查询和重播事件的完整列表以获取聚合的当前状态)实现的,并在内存中维护一份聚合的缓存副本。


命令处理程序调用由域模型公开的方法。


SeatAvailability记录了一个事件,其中包含预留的座位数。当下次聚合应用事件时,所有保留的(SeatAvailability)事件将用于计算剩余的座位数。


系统会将新事件追加到事件存储区中的事件列表中。


如果用户取消了一个座位,系统将遵循类似的过程,除了处理程序发出的命令,会生成一个取消座位的事件并将其追加到事件存储区。


除了提高系统的可伸缩性之外,使用事件存储还提供了一个会议的预订和取消的完整历史记录用于审核跟踪。事件存储区中的事件是准确的记录。不需要再存储其他信息,因为系统可以轻松地重播事件并将状态恢复到任何时间点。


在介绍事件源时,可以参考有关此示例的更多信息。(https://msdn.microsoft.com/library/jj591559.aspx)


相关的模式和指导
在实现此模式时,以下模式和指导也可能是相关的:命令和查询职责隔离(CQRS)模式(https://docs.microsoft.com/en-us/azure/architecture/patterns/cqrs)。为CQRS 实现提供永久信息源通常是基于事件源模式的实现。本文描述了如何将应用程序中读取数据的操作与使用独立接口更新数据的操作隔离。


实例化视图模式(https://docs.microsoft.com/en-us/azure/architecture/patterns/materialized-view).基于事件源的系统中使用的数据存储通常不适合高效查询。相反,通常的做法是定期生成数据的填充视图,或在数据发生变化时。该文说明如何完成此操作。


补偿事务模式(https://docs.microsoft.com/en-us/azure/architecture/patterns/compensating-transaction)。事件源存储中的数据不会被更新,而是添加了新项,将实体状态转换为新值。若要撤销更改,需要使用补偿项,因为无法直接反转以前的更改。该文描述了如何撤消以前的操作所执行的工作。


数据一致性入门(https://msdn.microsoft.com/library/dn589800.aspx)。在单独的读取存储或实例化视图中使用事件源时,读取数据不会立即一致,只能是最终一致。本文总结了一些分布式数据一致性的问题。


数据分区指南 (https://docs.microsoft.com/en-us/azure/architecture/best-practices/data-partitioning)。在使用事件源来提高可伸缩性、减少争用以及优化性能时,通常会对数据进行分区。本文描述如何将数据划分为离散分区以及这样做可能出现的问题。


格雷格.杨的帖子为什么要使用事件来源:(http://codebetter.com/gregyoung/2010/02/20/why-use-event-sourcing/)

相关文章:

  • 新加坡Ruby开发者6月聚会
  • Azure设计模式之外配存储模式
  • [Web 开发] URL 的最大长度
  • Azure设计模式之联邦身份模式
  • 从Oracle到DB2(四)--字符集
  • Azure设计模式之看门人模式
  • 圣淘沙闲逛,傻照两张!
  • Azure设计模式之网关模式
  • Azure设计模式之网关卸载模式
  • Makefile与Shell的问题
  • jQuery:收集一些基于jQuery框架开发的控件/jquery插件。(2)
  • Azure设计模式之网关路由模式
  • Azure设计模式之端点监控模式
  • linux-2.6.26内核中ARM中断实现详解(3)
  • Azure设计模式之索引表
  • 【前端学习】-粗谈选择器
  • CAP 一致性协议及应用解析
  • React as a UI Runtime(五、列表)
  • Shell编程
  • vue中实现单选
  • 闭包,sync使用细节
  • 关于for循环的简单归纳
  • 理解 C# 泛型接口中的协变与逆变(抗变)
  • 力扣(LeetCode)21
  • 买一台 iPhone X,还是创建一家未来的独角兽?
  • 使用 Node.js 的 nodemailer 模块发送邮件(支持 QQ、163 等、支持附件)
  • 微信小程序:实现悬浮返回和分享按钮
  • 我是如何设计 Upload 上传组件的
  • AI算硅基生命吗,为什么?
  • HanLP分词命名实体提取详解
  • 宾利慕尚创始人典藏版国内首秀,2025年前实现全系车型电动化 | 2019上海车展 ...
  • 如何用纯 CSS 创作一个货车 loader
  • #14vue3生成表单并跳转到外部地址的方式
  • #pragma once与条件编译
  • #QT(TCP网络编程-服务端)
  • #我与Java虚拟机的故事#连载04:一本让自己没面子的书
  • (webRTC、RecordRTC):navigator.mediaDevices undefined
  • (多级缓存)缓存同步
  • (二)学习JVM —— 垃圾回收机制
  • (分享)一个图片添加水印的小demo的页面,可自定义样式
  • (附源码)ssm经济信息门户网站 毕业设计 141634
  • (附源码)计算机毕业设计SSM教师教学质量评价系统
  • (附源码)计算机毕业设计SSM在线影视购票系统
  • (一) springboot详细介绍
  • (一)WLAN定义和基本架构转
  • (原創) 博客園正式支援VHDL語法著色功能 (SOC) (VHDL)
  • (转)【Hibernate总结系列】使用举例
  • (转)ABI是什么
  • (转)JAVA中的堆栈
  • *ST京蓝入股力合节能 着力绿色智慧城市服务
  • .[hudsonL@cock.li].mkp勒索病毒数据怎么处理|数据解密恢复
  • .NET Standard / dotnet-core / net472 —— .NET 究竟应该如何大小写?
  • .Net Winform开发笔记(一)
  • .net 逐行读取大文本文件_如何使用 Java 灵活读取 Excel 内容 ?
  • .NET/C# 使用 #if 和 Conditional 特性来按条件编译代码的不同原理和适用场景