Skip to main content
  1. internet/

事件驱动系列—背景

·1529 words·4 mins·

场景 #

同步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 #

集成事件用于服务之间的消息通知。通常来说,发送方并不关心有多少个订阅方。

这种事件对于订阅双方来说是一种协议,发送方需要保证事件结构不能改变,如果需要改变,则要使用不同的版本号。其他两个事件只用于服务内部,因此无需做版本处理。

小结 #

随着软件的复杂度提升,对模块的解耦、数据的一致性的要求都会越来越高,而事件驱动在这些地方能够发挥关键作用,因此掌握事件驱动的技术和思想已成为必备的技术修炼。

推荐读物 #

  1. Event-driven systems & architecture with Chris Richardson, Indu Alagarsamy & Viktor Stanchev (Go Time #297) |> Changelog