事件驱动系列—背景
Table of Contents
场景 #
同步vs异步 #
从业务处理是否要等待结果的角度看,可以将处理方式分为同步和异步两种。
当我在做技术设计而对比同步和异步的优缺点时,我突然意识到这是两种东西,因此也就失去了比较的意义。
所有需要考虑是使用同步还是异步的问题,实际上都有一个明确的答案。
或者我们可以更进一步——如何”异步“!
以一个注册场景为例:用户注册后,需要将其加入到通知列表,并发送邮件以提示完成注册(加入通知列表和发送邮件都是通过http调用的其他服务接口)。
func SignUp(u User) error {
if err := CreateUserAccount(u); err != nil {
return err
}
if err := AddToNewsletter(u); err != nil {
return err
}
if err := SendNotification(u); err != nil {
return err
}
return nil
}
首先,这三步能不能同步执行?当然可以!创建账号、加入通知列表、发送邮件这三步加在一起也花费不了多长时间,因此不会造成接口超时,也不会让用户体验变差。
但如果加入通知列表失败了怎么办?
或许我们可以回滚创建的账户:
func SignUp(u User) error {
if err := CreateUserAccount(u, func() error {
if err := AddToNewsletter(u); err != nil {
return err
}
if err := SendNotification(u); err != nil {
return err
}
return nil
}); err != nil {
return err
}
return nil
}
func CreateUserAccount(u User, afterCreated func() error) error {
tx := db.Transaction()
if err := tx.Create(u); err != nil {
tx.Rollback()
return err
}
if err := afterCreated(); err != nil {
tx.Rollback()
}
tx.Commit()
return nil
}
改造后的CreateUserAccount会在一个事务中创建账号,并执行加入通知列表和发送邮件的逻辑,如果有错误产生,则回滚代码。
但是如果加入列表成功了,但是发送邮件失败会怎样?
刚创建的账号会被回滚,那么通知列表的”用户“就变成了幽灵。
既然同步的方式解决不了问题,那么异步就可以解决问题吗?如果只是简单的将操作变为异步,当然也解决不了问题,但解决这个问题的方式一定是异步的。
比如有一种设计模式叫outbox
,就是将加入列表和发送邮件抽象为消息,然后将消息保存到数据库中,然后再不断读取数据库中未消费的消息,并执行相对应的操作。为什么要将消息保存到数据库中?因为既然都是数据库操作,那就可以使用事务来保证一致性了!
DDD基础设施 #
DDD关注于领域规则,子域之间通过事件来通信,这使得各个子域之间能够实现高度的解耦。因此,领域驱动设计中规定:一个事务中只能包含一个聚合中的操作,不同聚合之间的”通信“要通过事件来驱动。
而DDD的这些需求,与事件驱动的架构完美的契合在一起。事件驱动已经成了DDD架构中不可缺少的一部分。
图片来自于Domain-Driven Design (DDD) in Modern Software Architecture | Bits and Pieces (bitsrc.io)
可溯的事件流 #
大数据平台往往需要收集、清洗业务系统的数据,这个收集的操作往往是通过事件进行的。
在这个过程中,如果将事件汇总在一起,并存入一个我们称之为event sourcing
的地方,就形成了一条完整的事件流。
既然有了这条事件流,那么当某个系统的数据出现错乱后,就可以从数据湖中的某个时间开始回溯这些事件,从而达到修复数据的目的。
区块链就是这样一种事物。之前有个钱包应用由于开发问题导致数据出现了问题,但由于团队将交易事件都保存在区块链中,因此读取完整的区块链便能将应用中的数据回溯到正常状态。
事件类型 #
图片来自《Event-Driven Architecture in Golang》第6章
Domain Event #
领域模块是一个服务中最核心的部分,领域事件就是用于Application层中不同领域模块之间进行通信。
领域事件通常不会暴露给外部服务。
Event Sourced Events #
事件源通常存储于服务内部,用于追踪事件相关的状态变更。
Integration Event #
集成事件用于服务之间的消息通知。通常来说,发送方并不关心有多少个订阅方。
这种事件对于订阅双方来说是一种协议,发送方需要保证事件结构不能改变,如果需要改变,则要使用不同的版本号。其他两个事件只用于服务内部,因此无需做版本处理。
小结 #
随着软件的复杂度提升,对模块的解耦、数据的一致性的要求都会越来越高,而事件驱动在这些地方能够发挥关键作用,因此掌握事件驱动的技术和思想已成为必备的技术修炼。