什么是事件溯源
Martin Fowler 在2005年的博客中提及了“Event Sourcing”这个词语,他将事件描述为一个应用的一系列状态改变,这一系列事件能够捕获用来重建当前状态的一切事实真相。他认为事件是不可变的,事件日志是一种只会不断追加(append-only)的存储。事件从来不会被删除,这意味着事件可以被重播。
Greg Young是另外一个在事件溯源领域的著名专家,他将事件溯源描述为一系列事件存储状态,然后通过播放这一系列事件重建系统状态。按照他的观点,事件日志处理只会不断追加事件,事件是已经发生的,不能再被改变(undo)。Fowler称这些事件为追溯事件,而Young称之为逆转交易(事务)。从这里看出,事件溯源的事件不一定是领域事件,追溯事件是来自聚合发出的用于重建状态的领域事件,而领域事件是更广泛的一种事件,可以用于在不同有界上下文之间实现通信。两者有时可合二为一,有时要注意区分,不是所有的领域事件都可以用来重建状态,用作追溯事件。
上面的内容是来自DDD专家的解释,其实很好理解,在我们的MVC架构和MVVM架构中,我们经常使用贫血模型来映射ORM,在一般的业务场景下,我们看到的是最终的结果,这个是什么意思呢?我来举个例子:在电商业务中,用户下了一个订单,然后在下单的时候,发现校验用户余额不足,此时提醒用户下单失败,此订单结束或者订单继续执行,直到用户取消订单或者订单支付超时或者订单支付成功。此时这个订单结束了,用户看到的最终的状态就是订单支付完成或者订单支付失败,其直接查询DB即可,但是查到的是最终的结果。现在有个需求,我需要知道这条订单的执行轨迹,我且称呼其为生命周期,用户从下单开始,一条订单的生命周期开始,此时不考虑其是否生成订单号等等信息,我们此处只谈论这个周期的事情。名人会有一个自传,其自传就是讲述其的一生,而此时订单的一生如何描述?订单出生就死亡了吗,这显然是不合理的,订单在他的生命中肯定有其精彩的事情,这不可抹去。而这些发生的事情,就是订单的业务属性。假如我想知道订单的流程,可以通过怎样的方式?可以通过日志记录的方式,描绘订单执行的流程,在哪一个步骤失败、失败原因是什么、订单执行了什么业务逻辑等等,而这可以算是简单的事件溯源了。每一条日志就是一个“事件”。
事件溯源的优点
事件溯源的特点在于注重“事件集合”这个概念,将一个个领域中发生的事件作为领域状态改变的唯一真相,这一看似简单的关注点的改变,却带来了很多意想不到的巨大效果。
首先,重视事件导致分析方式重点的改变,以前是重点分析可变的状态,现在重点是分析领域中的事件,因此,事件风暴分析出的领域事件也可以直接作为事件溯源的事件来源,事件风暴会议中寻找的各种领域事件,实际是在寻找各种动作明细记录。但是注意不是所有领域事件都可以作为追溯事件,有些领域事件只是其他有界上下文比较感兴趣所以关注订阅了而已,追溯事件是关注订阅那些改变聚合状态的重要领域事件,也可以将事件溯源和其他有界上下文一样作为一个领域事件的订阅者看待。
其次,重视领域中重大的可追溯事件,其实就是重视“变更数据的捕获”(英文简称CDC),这对分布式系统中进行状态同步非常有利,通过将一台机器上的变更日志(事件日志)广播到其他机器上,再在其他机器上重播这些事件日志,相当于将状态复制到其他机器上。这相比直接状态复制的好处是,能够将逻辑顺序复制过去。状态直接复制很难判断几个状态值的前后顺序,在分布式环境中很难统一全局的物理时间,因此无法根据真正的物理时间来判断前后顺序,这一点在分散式系统中更加明显,分散式系统比分布式系统更加去中心化。
其他好处有
- 提供监管区域(如金融业)、政府法规中规定的审计日志。在许多国家,要求公司保存系统运行的记录。例如,美国的法规要求公司以不可重写、不可擦除的格式保存记录事件来源,由于事件溯源只附加事件日志和不可变事件特性,所以非常适合这一要求。
- 一次写入多读(WORM)数据存储等技术可与事件溯源互补使用,WORM存储能在硬件级别防止数据更改,并且只允许附加新数据。
- 在硬件级别就防止数据更改,并且只允许附加新数据。
- 调试那些已经捕获的事件可用于进一步了解系统为什么会达到当前状态,哪些事件造成了当前状态。事件溯源在可跟踪性和调试能力方面体现了优势。
- 事件溯源还有可伸缩性的优势,只追加的事件日志是同步复制状态的唯一方法。相比状态字段锁而言,由于复制不可变的事件日志几乎不需要用锁,在CQRS中,复制事件日志比直接复制状态更易于扩展。
- 信息价值也是应用事件溯源的一个动机,检查事件日志可以重建或查询系统的所有过去状态。这可以为系统提供很大的价值,特别是当分析交互作用时。在这样的系统中,通常不知道将来要做什么样的分析。例如一个在线商店,商家老板希望列出顾客放入购物车或取走的所有商品列表,这样能够比较商品的受欢迎程度,使用事件溯源架构可以很容易遍历这个购物车事件集合并罗列出来。
现在很多人口口相谈的大数据、数据挖掘,那么重要的一点是,数据从何而来?没有数据只是空谈。
使用事件溯源可以代替分布式事务,传统分布式事务主要是2PC——两段事务提交,JavaEE的JTA/XA是2PC的一种实现。过去相当长的一段时间里,它是构建企业分布式系统的实际标准。值得注意的是,2PC是原子提交协议,这意味着如果所有参与者都投票“是”,则所有参与者最终都将提交,否则将使系统保持不变。2PC本身不提供ACID事务实现机制,它只是一种协调机制。
两段提交协议是一种分布式资源的原子确认协议,这个协议通过两段过程完成业务数据的更改:第一段是预准备阶段,事务管理器通知所有分布式资源准备接受提交或退出事务;第二阶段,事务管理器根据每个分布式资源的回应情况决定是真正提交完成事务还是退出事务。在第一阶段,修改的数据并没有真正写入数据库,只有最后阶段完成时才真正写入。
2PC非常依赖系统的参与者和协调者之间的信任,适合用于用户高度可信的可控环境。
微服务中分布式事务实现
可以引入Saga模式。流程中任何一个环节执行失败,之前执行的环节都要进行回退,Saga模式就是这种补偿式、回退的流程设计。Saga于1987年首次应用于海克特和肯尼思·萨利姆的研究论文。它是作为长时间分布式事务的概念替代方案而引入的。Saga是将较大的事务分成一组较小的事务,这减少了必须在数据库资源上保留锁的时间,因此可以避免瓶颈。每个较小的交易都可以通过补偿动作撤销。如果中途必须中止一个大型事务,则必须对已经完成的所有较小交易进行补偿回退。因此,本质上Saga就是一种补偿模式。
以计划一次旅行为例。为了实现旅行这个功能,需要三个步骤,第一步预订出租车,第二步预订酒店,然后预订机票。如果预订机票时没有机票了,那么这段旅程只能取消,将酒店退订,出租车退租,这都是业务上发生的动作和事件。这些都可以在事件风暴会议中详细涉及,也就是说,分布式事务不再一股脑儿地推给分布式事务中间件,而是从业务上来建模实现。
当一个流程中任何一个环节发生异常(比如预订机票出错)时,那么取消之前的环节(如取消预订酒店,取消出租车),这样整个流程就好像没有执行一样,这是这段流程取消的真正业务含义。
使用事件溯源方式实现Saga需要以下三个部件
- 持久的Saga事件日志:记录流程相关的事件集合。
- Saga协调器:类似数据库中的流程锁,这个锁管理整个流程的状态,除此以外,协调者还负责流程的下一步推动,或者回到上一步的回退推动。
- 补偿行为的幂等性:幂等性可以指流程中某个步骤节点能够重复执行多次,但是不会影响应用状态,当然如果影响了,也可以去除这种影响。这是由流程中每个步骤节点保证的。
使用分布式中间件实现
事件溯源的实现关键是实现事件日志的存储,虽然传统关系数据库可以实现事件日志存储,但是无法保证直接通过SQL去修改数据表中的数据,而事件日志的特点是不断追加新增,不能修改删除,因此,使用一种更符合事件日志特点的存储系统将能保证事件溯源的完整实现,当然前提是该领域适合使用事件溯源。
Apache Kafka是一个基于事件日志的消息系统,其内部有一个日志,是一个强有序的记录序列,每个记录都被分配一个顺序的数字偏移量,用于标识日志中的记录位置。生产者将记录追加到此日志,多个消费者应用程序从各自指定的偏移量读取消息。
生产者不断追加消息到日志中,消费者有自己的偏移量(offset)指针,表示自己读取这个日志的位置。每次读取消息处理完毕,这个偏移量指针会增加一格,向后移动,这样可以读取不断增长的日志中的消息数据。不同消费者有自己的偏移量。
使用Kafka存储事件有三种可能的策略
- 将所有实体类型的所有事件存储在一个主题中(具有多个分区)。
- 每个聚合根实体类型对应一个主题,例如所有用户的相关事件放在一个单独主题下,所有与产品相关的事件放在一个主题里。
- 每个聚合根实体实例对应一个主题,例如每个用户实例和每个产品实例都有一个单独主题。
第三种策略是不可行的。如果系统中的每个新用户都需要创建新主题,就会得到无限数量的主题。而第一和第二种策略的优点是:只需一个主题就可以更容易地获得所有事件的全局视图。另一方面,对于每个聚合根实体类型对应一个主题,可以分别对每个实体类型流进行分区和扩展。这两种策略之间的选择取决于用例,常用的是第二个策略,一个聚合根类型对应一个主题。事件溯源建模章节分析过,一个事件日志/集合等同于一个活动的概念,等同于一个聚合,一个活动包含一个事件日志,这个活动对应Kafka的一个主题。
投射模式
投射模式是事件溯源中使用的核心模式之一,正如其名称一样,它是将事件流投射成状态,从事件流中得出当前状态。将一系列事件序列(也称为“流”)用于重建当前状态,以便可以处理任何后续请求。当前状态是大多数系统比较关心的主要数据之一。
投射成的状态结果可以有几种保存方式:内存、关系数据库、NoSQL和文件系统。
在现实生活中,投影实现往往包含两个部分
- 一个库,允许查找或存储状态。
- 一个投影器,也就是事件处理程序,它知道如何更新或创建状态。
更改数据捕获(CDC)
CDC是一种设计模式,可以持续识别并捕获数据的增量更改,专门用于从现有数据库中复制所有的SQL操作事件。大多数现代数据库通过事务日志支持CDC。事务日志是对数据库所做所有更改的顺序记录,而实际数据包含在单独的文件中。
CDC是当前最常用的事件溯源衍生物,两者虽然有区别,但是事件日志是它们的共同点。虽然是衍生的事件溯源,但它是现有传统关系数据库基础上的变通,因此针对遗留系统使用事件溯源也非常有意义,也可以为企业逐步迈向事件溯源架构提供演进步骤。
Debezium是为CDC构建的分布式平台,它使用数据库事务日志并在行级更改时创建事件流,侦听这些事件的应用程序可以基于增量数据更改来执行所需的操作。Debezium提供了一个现有数据库的连接器库,支持当今可用的各种数据库。这些连接器可以监视和记录数据库模式中的行级更改,然后将更改发布到诸如Kafka的流服务上。通常,将一个或多个连接器部署到Kafka Connect集群中,并配置为监视数据库,还将数据更改事件发布到Kafka。分布式Kafka Connect群集可提供所需的容错能力和可伸缩性,从而确保所有已配置的连接器始终处于运行状态。
小结
产品溯源基本上完全是事件溯源的精确应用场景,因此,事件溯源和区块链一样,应用范围非常广泛,事件溯源可以还原事故现场,其反馈的是一个个真实的案例和场景,而非模拟的虚假的行为和信息。
假如人们买了一个产品,如有机生态猪肉、牛肉或水果,在区块链技术下,可以知道产品从生产到流通的全过程,这其中有政府的监管事件记录、专业的检测事件记录、企业的质量检验事件记录等。智慧的供应链将使日常吃到的食物、用到的商品更加安全。
事件溯源是过程,而数据库存储是结果。结果流动然后串行,就是一个生命周期。
参考文章
- 复杂软件设计之道:领域驱动设计全面解析与实战