本篇主要总结第六章:使用事件溯源开发业务逻辑
传统的持久化技术
将类映射到数据库表,将类的字段映射到数据表中的列,类的实例映射到数据表中的行。比较成熟的ORM,像Mybatis、JPA等。
作者上来就指出传统持久化技术的弊端,但是看完本章后个人感觉,在更多的项目实践中,还是传统的持久化技术更香,原因就一个——技术成熟。来看看作者说的弊端:
- 对象与关系的“阻抗失调”(impedance mismatch)
- 这个缺点在字面上有点费解,其实就是说类和对象是面向对象的产物,面向对象的思想与关系型数据的准则之间本身不是相辅相承的关系。越是复杂的面向对象系统设计,越是难以用关系型数据库来表达。
- 缺乏聚合历史
- 就是只有记了最终状态没记录对象的历史变化(但是大多数的传张后台系统都会有审计日志的需求,其实还是变相记录了部分对象历史的)
- 实施审计功能将非常烦琐且容易出错
- 事件发布凌驾于业务逻辑之上
- 事件发布与业务逻辑代码不能完全同步
事件溯源
事件溯源将每个聚合作为一系列事件来持久化保存,每个事件代表聚合状态的一次改变。
采用事件溯源后,聚合中具体业务逻辑的方法,都要改造成process+apply的方式实现:
- process:接受外部调用并生成事件,但不更改聚合的状态
- apply:接受事件并修改聚合状态
这是一个侵入性较强的操作,如果之前不熟悉事件溯源开发,可能会在这实施项目时遇到问题。而且创建和更新聚合都需要遵守先加载重放,再生成事件,然后再apply事件的模式。
创建聚合的步骤:
- 使用聚合的默认构造函数实例化聚合根
- 调用process()以生成新事件
- 遍历新生成的事件并调用apply()来更新聚合的状态。
- 将新事件保存在事件存储库中。
更新聚合的步骤:
- 从事件存储库中加载聚合事件
- 使用基默认构造函数实例化聚合根
- 遍历加载的事件,并在聚合根上调用apply()方法进行事件重放
- 调用其process()方法以生成新事件
- 遍历新生成的事件并调用apply()来更新聚合的状态。
- 将新事件保存在事件存储库中。
实施事件溯源还需要注意:
- 如果聚合的事件非常多,导致重放效率越来越低,则需要考虑引入聚合的快照(Snapshot),首先选择合理的快照,再重放快照之后的事件得到最新的聚合。
- 使用乐观锁解决同时更新同一聚合的情况。
- 事务溯源还必须实现消息处理的幂等。
- 需要解决事件演化更新的问题,确保不同历史版本的事件可以兼容最新的聚合重放。
事件溯源的好处
- 可靠地发布领域事件
- 保留聚合的历史
- 最大限度地避免对象与关系的“阻抗失调”问题
- 为开发者提供一个“时光机”
事件溯源的弊端
- 有一定的学习曲线
- 基于消息传递的应用程序的复杂性
- 处理事件的演化更新有一定难度
- 删除数据存在一定难度
- 查询事件存储库非常有挑战性
学习总结
事件溯源是一个业务逻辑设计的新颖模式。区别于传统的业务逻辑存储”结果“,把对象的最新状态通过ORM或者直接数据库操作存储在数据库中。事件溯源的理念是存储”过程“,把对象每次状态变化都以事件记录的形式存放在数据库中。
我理解事件溯源最直接的两个好处:
- 第一,不再需要复杂的数据库表设计,理论上一个事件储存表就可以应用到所有对象上。
- 第二,做审计功能的时候,后端几乎没有额外的工作量,天然就支持。
而要真正实现事件溯源要做的准备不少:
- 首先,要学会使用事件溯源;
- 其次,如何定义较好抽象的事件也是一个让开发人员头痛的事情,开发人员之间必须明确理解每个事件的意义,防止出现重复冗余的事件。
- 第三,每个事件可能包含的数据不同,如果要统一格式存储,必然要用到序列化为文本或二进制的通用字段。这对于查询的业务逻辑来说,编码将非常麻烦。比如,要找出某个属性值为5的所有聚合的实例,需要加载并重放所有聚合,然后再查询属性为5的聚合实现。
因此,我觉得事件溯源更适合于对历史事件要求非常苛刻的项目,一般的业务需求中,只需要在对象的几个关键节点上记录事件即可。微
参考文献:
Event Stream Processing, Streaming Data, and CEP Explained