过去在学Actor模型的时候,就认为异步消息是相当的重要,在华为的时候,也深扒了一下当时产品用的消息模型,简单实用,支撑起了很多模块和业务,但也有一个缺点是和其他的框架有耦合,最近看到以太坊的事件框架,同样简单简洁,理念很适合初步接触事件框架的同学,写文介绍一下。
以太坊的事件框架是一个单独的基础模块,存在于目录go-ethereum/event
中,它有2中独立的事件框架实现,老点的叫TypeMux
,已经基本弃用,新的叫Feed
,当前正在广泛使用。
TypeMux
和Feed
还只是简单的事件框架,与Kafka、RocketMQ等消息系统相比,是非常的传统和简单,但是TypeMux
和Feed
的简单简洁,已经很好的支撑以太坊的上层模块,这是当下最好的选择。
TypeMux
和Feed
各有优劣,最优秀的共同特点是,他们只依赖于Golang原始的包,完全与以太坊的其他模块隔离开来,也就是说,你完全可以把这两个事件框架用在自己的项目中。
TypeMux
的特点是,你把所有的订阅塞给它就好,事件来了它自会通知你,但有可能会阻塞,通知你不是那么及时,甚至过了一段挺长的时间。
Feed
的特点是,它通常不存在阻塞的情况,会及时的把事件通知给你,但需要你为每类事件都建立一个Feed,然后不同的事件去不同的Feed上订阅和发送,这其实挺烦人的,如果你用错了Feed,会导致panic。
接下来,介绍下这种简单事件框架的抽象模型,然后再回归到以太坊,介绍下TypeMux
和Feed
。
事件框架的抽象结构
如上图,轻量级的事件框架会把所有的被订阅的事件收集起来,然后把每个订阅者组合成一个列表,当事件框架收到某个事件的时候,就把订阅该事件的所有订阅者找出来,然后把这个事件发给他们。
它需要具有2个功能:
- 让订阅者订阅、取消订阅某类事件。
- 让发布者能够发布某个事件,并且把事件送到每个订阅者。
如果做成完善的消息系统,就还得考虑这些特性:可用性、吞吐量、传输延迟、有序消息、消息存储、过滤、重发,这和事件框架相比就复杂上去了,我们专注的介绍下以太坊的事件模型怎么完成上述3个功能的。
以太坊的事件模型
TypeMux
是一个以太坊不太满意的事件框架,所以以太坊就搞了Feed
出来,它解决了TypeMux
效率低下,延迟交付的问题。接下来就先看下这个TypeMux
。
TypeMux:同步事件框架
**TypeMux是一个同步事件框架。**它的实现和上面讲的事件框架的抽象结构是完全一样的,它维护了一个订阅表,表里维护了每个事件的订阅者列表。它的特点:
- 采用多对多结构:多个事件对多个订阅者。
- 采用推模式,把事件/消息推送给订阅者,就像信件一样,会被送到你的信箱,你在信箱里取信就行了。
- 是一个同步事件框架。这也是它的缺点所在,举个例子就是:邮递员要给小红、小明送信,只有信箱里的信被小红取走后,邮递员才去给小明送信,如果小红旅游去了无法取信,邮递员就一直等在小红家,而小明一直收不到信,小明很无辜无辜啊!
看下它2个功能的实现:
- 订阅和取消订阅。订阅通过函数
TypeMux.Subscribe()
,入参为要订阅的事件类型,会返回TypeMuxSubscription
给订阅者,订阅者可通过此控制订阅,通过TypeMuxSubscription.Unsubscribe()
可以取消订阅。 - 发布事件和传递事件。
TypeMux.Post()
,入参为事件类型,根据订阅表找出该事件的订阅者列表,遍历列表,依次向每个订阅者传递事件,如果前一个没有传递完成进入阻塞,会导致后边的订阅者不能及时收到事件。
TypeMux源码速递
TypeMux
的精简组成:
// A TypeMux dispatches events to registered receivers. Receivers can be
// registered to handle events of certain type. Any operation
// called after mux is stopped will return ErrMuxClosed.
//
// The zero value is ready to use.
//
// Deprecated: use Feed
// 本质:哈希列表,每个事件的订阅者都存到对于的列表里
type TypeMux struct {
mutex sync.RWMutex // 锁
subm map[reflect.Type][]*TypeMuxSubscription // 订阅表:所有事件类型的所有订阅者
stopped bool
}
订阅:
// Subscribe creates a subscription for events of the given types. The
// subscription's channel is closed when it is unsubscribed
// or the mux is closed.
// 订阅者只传入订阅的事件类型,然后TypeMux会返回给它一个订阅对象
func (mux *TypeMux) Subscribe(types ...interface{
}) *TypeMuxSubscription {
sub := newsub(mux)
mux.mutex.Lock()
defer mux.mutex.Unlock()
if mux.stopped {
// set the status to closed so that calling Unsubscribe after this
// call will short circuit.
sub.closed = true
close(sub.postC)
} else {
if mux.subm == nil {
mux.subm = make(map[reflect.Type][]*TypeMuxSubscription)
}
for _, t := range types {
rtyp := reflect.TypeOf(t)
// 在同一次订阅中,不要重复订阅同一个类型的事件
oldsubs := mux.subm[rtyp]
if