Golang解耦系列,第4篇:事件总线

2,169 阅读3分钟

eventBus 事件总线

English Document中文文档English Documentgithub Source

包:"github.com/farseer-go/eventBus"

模块:eventBus.Module

概述

以事件驱动的方式来解耦业务逻辑,在DDD中,事件总线是必然用到的技术。

当两个业务模块相互之间有业务关联,但又不希望在代码结构上直接依赖。

则可以使用事件驱动的方式来解耦相互之间的依赖。

我们模拟一个场景:用户下单之后,我们要做很多事情:

  1. 保存订单信息
  2. 修改库存
  3. 生成仓库待发货通知
  4. 给用户增加积分

这时代码可能是:

func NewOrder() {
    saveOrder()
    updateStock()
    warehouseNotification()
    userAddPoints()
}

当我们代码写完发布上线后,产品经理过来说,需要增加用户短信通知:

func NewOrder() {
    saveOrder()
    updateStock()
    warehouseNotification()
    userAddPoints()
    userPhoneSms()
}

如果哪天产品经理又来.....又加需求,这时我们就无法保证NewOrder()函数的稳定性,需求一来就得改代码。 这还不是主要的,不知道小伙伴们发现没,订单、库存、仓库通知、用户积分、短信,被我们硬生生的整合到一起了, 更别提又来什么奇怪的需求。

解析办法就是,使用事件通知。然后让各个功能需求来订阅。每增加一个功能,我们不用再修改NewOrder()函数了,而是添加新的xxx.go,然后通过订阅方式去实现。

当然,我们也可以使用上篇讲的Golang解耦神器,第三篇:Rabbit消息中间件达到同样的目的。

1、发布事件

本着farseer-go极简、优雅风格,使用eventBus组件也是非常简单的:

函数定义

// 发布事件(同步、阻塞)
func PublishEvent(eventName string, message any)

// 发布事件(异步)
func PublishEventAsync(eventName string, message any)
  • eventName:事件名称
  • message:事件消息

演示:

type newUser struct {
    UserName string
}

func main() {
    fs.Initialize[eventBus.Module]("queue生产消息演示")

    // 同步(阻塞)
    eventBus.PublishEvent("new_user_event", newUser{UserName: "steden"})

    // or 异步(非阻塞)
    eventBus.PublishEventAsync("new_user_event", newUser{UserName: "steden"})
}

2、订阅事件

函数定义

// 订阅
func Subscribe(eventName string, fn core.ConsumerFunc)
// 回调函数
type consumerFunc func(message any, ea EventArgs)
  • eventName:事件名称
  • fn:事件回调函数
  • message:事件消息
  • ea:事件参数

演示:

type newUser struct {
    UserName string
}

func main() {
    fs.Initialize[eventBus.Module]("queue生产消息演示")

    eventBus.Subscribe("new_user_event", func (message any, ea core.EventArgs) {
        user := message.(NewUser)
        // do.....
    })
}

3、使用IOC

在上文中,我们会显示的依赖eventBus包,如果您想使用接口方式,也是支持的:

eventBus.RegisterEvent("TaskScheduler", domainEvent.SchedulerEvent)

// SchedulerEvent 任务调度
func SchedulerEvent(message any, _ core.EventArgs) {
   ...
}

首先我们在初始化阶段,使用eventBus.RegisterEvent注册一个事件,事件名称为:TaskScheduler,事件订阅:domainEvent.SchedulerEvent

接着可以使用container包来解析接口,并发布事件:

event:= container.Resolve[core.IEvent]("TaskScheduler")
event.Publish(....)

同样支持注入的方式:

type TaskGroupMonitor struct {
   SchedulerEventBus core.IEvent `inject:"TaskScheduler"` // 任务调度事件
}

func (receiver *TaskGroupMonitor) waitScheduler() {
    _ = receiver.SchedulerEventBus.Publish(receiver)
}

4、使用Redis的发布订阅

eventBus包(模块),是在当前进程实现的发布与订阅。如果你希望你的事件能被其它机器上的实例执行。我们可以使用farseer-go/redis包来实现

// 注册客户端更新通知事件
redis.RegisterEvent("default", "ClientUpdate", domainEvent.ClientUpdateEvent)

// ClientUpdateEvent 客户端有更新(Redis订阅)
func ClientUpdateEvent(message any, _ core.EventArgs) {
    var clientDO *client.DomainObject
    _ = json.Unmarshal([]byte(message.(string)), clientDO)
    // doing....
}

上面这段是注册事件的实际例子,接下来看下事件的发布:

type clientRepository struct {
   ClientUpdateEventBus core.IEvent `inject:"ClientUpdate"`
}

func (receiver *clientRepository) Save(do *client.DomainObject) {
    // 发到所有节点上
    _ = receiver.ClientUpdateEventBus.Publish(do)
}

使用上是一样的,只是实现的对象从eventBus换成了redis。对于事件发布者而言,并不感知是使用哪种实现发布的。

这是因为不管是eventBus还是redis,都共用了core.IEvent这个接口,搭配container(IOC)技术,实现了技术细节的解耦。

使用redis事件,可以实现集群广播效果,一个事件发布后,所有机器都能接收。