DDD事件溯源

背景

         在客户使用一些系统时,有时会出现一些用户不能理解的情况,很可能认为是系统的缺陷,当该系统没有业务操作的相关历史记录时就难以分辨。如果能把这些用户操作都记录下来,出现问题时可以分析这些记录,就能确定是客户的正常操作所致还是程序的确存在问题,在和客户沟通时可以做到有理有据,增加客户的信任。有些系统要进行合规性审计,必须要有历史性操作记录。有时系统出现了问题,可以通过分析这些历史记录定位出问题,通过删除或修改历史事件,可以将系统恢复到之前的某个状态,以消除错误或回退系统变更。还可以分析历史记录来优化系统。

解决方案

        记录系统业务操作,形成历史记录的常用解决方案是使用事件溯源模式。它把成功的业务命令操作转换成领域实体或聚合(DDD 领域驱动设计的概念)的事件存储起来,在还原领域聚合或实体对象时,把记录的这些事件按照顺序读取,生成对应的领域聚合或实体。事件溯源常常和 CQRS 命令查询模式一起使用,通过命令事件存储和发布,各种查询展示视图根据这些命令事件更新,从而弥补了事件溯源不适合做查询展示的劣势。当记录事件越来越多时,生成领域聚合或实体的时长越来越长,所以一般会定期生成一个快照存储起来,在其基础上读取相关最近的事件,从而加快生成领域对象。

        事件溯源对事件只有追加,没有更新, 所以在多线程或微服务多实例中不像常规的对象状态修改那样通过加锁和事务来避免竞争性,从而提高并发的性能。

        事件溯源可以通过重播这些记录的历史事件还原系统状态、回滚更改或保留历史记录和审核日志。

        事件溯源不适合业务简单,几乎没有业务逻辑,很自然地通过传统 CURD 就能简单实现,无需采用复杂的领域模型。也不适合要求数据视图实时更新的系统,因为 CQRS 的数据视图是最终一致性,有一定延时,而实时更新显然是不行的,所以事件溯源模式不适合。

实现问题和注意事项

  • 事件溯源位置: 应该在网关下面的第一层,也就是通常的应用层。网关一般只是转发,基本没有业务逻辑相关处理,网关直接下层,应用层才会对用户的命令操作直接处理并产生溯源事件,事件更接近源头方便记录更多详细信息,通过重放这些事件方便后续的业务重构。
  • 事件粒度:领域的聚合作为事件产生维护的粒度,如果没有聚合,则以实体作为事件粒度。因为聚合或实体具有强一致性,也代表领域某个方面的完整描述,以此作为事件粒度最为合适。粒度过小,不利于一致性的维护,粒度过大内聚性不够,增加了耦合性。
  • 事件主要字段: 事件 id、聚合类型(实体类型)、聚合 id(实体 id)、事件类型、时间、用户 id、事件内容(通常 json 格式)。聚合类型、事件类型和事件内容格式是对应关联的,  聚合类型和聚合 id  确定了对象实例。
  • 事件生成方式:聚合在对命令操作成功后,生成事件队列,不更改聚合状态,只有把这些事件用于聚合上对应更新状态的方法时才会更新聚合状态。生成后事件会直接存入事件数据库,同时根据需要直接作为领域事件发布出去,不过一般建议通过数据库拖尾方式发布领域事件。 显然,这和常规的方式不同,命令操作的目的不再是更新领域状态,而是生成事件,再通过事件再去更新领域状态。在领域事件方面,直接用命令产生的聚合事件作为领域事件,无需特殊转换,这样领域事件的内容完全由生产者决定,消费者不能干涉。存储方面,也只是存储事件,不会存储聚合的当前状态。
  • 聚合的构造:事件溯源构造也和常规的不同,聚合或实体先构造一个空的实例对象,然后通过存储接口把所有该资源的历史事件全部查询出来,然后传递给该聚合实例对象的事件应用方法,通过加载应用所有的事件完成该聚合实例对象的创建。
  • 聚合的快照:随着操作越来越多,聚合的事件也越来越多,势必影响性能,为了加快聚合对象的构造,需要对该聚合进行快照,这样构造聚合对象时,先读取该资源的快照,然后再读取该快照后的事件把聚合更新到最新状态。快照方法一般是把聚合对象进行序列化,把序列化的结果和对应聚合根id、最新事件 id、时间存储起来,方便快照查询。快照间隔可以定期进行快照,例如每天一次,或者按事件次数来快照,新的快照替换旧的快照。
  • 事件存储: 一般推荐关系型数据库存储事件,常规的 mysql、PostgreSQL 等够用,如果数据量可能会很大时推荐使用 阿里的 oceanbsse 分布式数据库, 现在这些数据库都已经支持半结构的 json 类型了。非关系型数据库推荐使用 mongodb。 事件存储时,一般要建立三个表如下图,事件表(EVENTS)、快照表(SNAPSHOTS)、实体表(ENTITIES)。事件表和快照表顾名思义,上面对主要字段都已描述,不再赘述。 实体表的目的是为了实现乐观锁,entity_version 和对应实体最新事件 id 相同,每行数据表示一个聚合或实体,当有新事件时,先保存在事件表中,然后根据乐观锁的原理去更新实体表的对应的entity_version 为最新的事件 id,这一切都在事务中进行,如果乐观锁失败就引起事务回滚,撤销了事件的插入。如果关系数据库的采用隔离是可重复读级别的,那么就无需乐观锁的方式,直接更新版本就行,在并发修改同一行数据会导致其中晚提交事务的失败。随着时间积累数据会越来越大,影响性能,可以按照聚合或实体的名称来建立对应的这个数据库表,通过分表达到提升性能的目的。

          

  • 事件溯源并发问题:上面通过事务解决了存储时的并发问题,但在事件消费时如果需要顺序保护和幂等,就得参考“事件顺序保护”的内容,那里面有详细描述。 为了实现消费顺序性,需要在上面的事件结构里面增加事件序号,相同的实体类型和实体id 对应事件序号是递增的,后面通过该序号来保证事件消费顺序。
  • 事件溯源的版本升级:随着业务发展,事件的内容结构会进行变化,有的变化是向后兼容的,有的不是。可以在事件内容里加上版本号,通过版本号区分事件结构演变,但这样就需要保留各个事件版本的处理代码,特别是那么多事件消费者,都得保留历史事件版本的消费接口,这样代码越来越难维护。另一种方法,在每次事件演变时,把旧事件结构全部遍历转化成新事件的结构,相关的代码也只处理新事件结构的情况,这样就没有旧事件结构的维护,整个程序更加整洁。
  • 事件溯源框架库推荐:有利于事件溯源的 java 框架有 Axon Framework、Eventuate Tram。 Axon 是事件驱动架构和 CQRS 框架,虽然不直接提供事件溯源,但该事件框架很方便实现事件溯源。Eventuate Tram 里有完整的事件溯源,更加方便,但名气不如 Axon。

总结

        事件溯源的学习曲线较高,和常规编程方式有很大的区别,适合复杂的业务场景,需要 DDD 领域驱动设计的相关知识。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值