以太坊源码分析:事件框架
过去在学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
的精简组成:
1 | // A TypeMux dispatches events to registered receivers. Receivers can be |
订阅:
1 | // Subscribe creates a subscription for events of the given types. The |
取消订阅:
1 | func (s *TypeMuxSubscription) Unsubscribe() { |
发布事件和传递事件:
1 | // Post sends an event to all receivers registered for the given type. |
我上面指出了发送事件可能阻塞,阻塞在哪?关键就在下面这里:创建TypeMuxSubscription
时,通道使用的是无缓存通道,读写是同步的,这里注定了TypeMux是一个同步事件框架,这是以太坊改用Feed的最大原因。
1 | func newsub(mux *TypeMux) *TypeMuxSubscription { |
Feed:流式框架
Feed是一个流式事件框架。上文强调了TypeMux是一个同步框架,也正是因为此以太坊丢弃了它,难道Feed
就是一个异步框架?不一定是的,这取决于订阅者是否采用有缓存的通道,采用有缓存的通道,则Feed就是异步的,采用无缓存的通道,Feed就是同步的,把同步还是异步的选择交给使用者。
本节强调Feed的流式特点。事件本质是一个数据,连续不断的事件就组成了一个数据流,这些数据流不停的流向它的订阅者那里,并且不会阻塞在任何一个订阅者那里。
举几个不是十分恰当的例子。
- 公司要放中秋节,HR给所有同事都发了一封邮件,有些同事读了,有些同事没读,要到国庆节了HR又给所有同事发了一封邮件,这些邮件又进入到每个人的邮箱,不会因为任何一个人没有读邮件,导致剩下的同事收不到邮件。
- 你在朋友圈给朋友旅行的照片点了个赞,每当你们共同朋友点赞或者评论的时候,你都会收到提醒,无论你看没看这些提醒,这些提醒都会不断的发过来。
- 你微博关注了谢娜,谢娜发了个搞笑的视频,你刷微博的时候就收到了,但也有很多人根本没刷微博,你不会因为别人没有刷,你就收不到谢娜的动态。
Feed和TypeMux相同的是,它们都是推模式,不同的是Feed是异步的,如果有些订阅者阻塞了,没关系,它会继续向后面的订阅者发送事件/消息。
Feed是一个一对多的事件流框架。每个类型的事件都需要一个与之对应的Feed,订阅者通过这个Feed进行订阅事件,发布者通过这个Feed发布事件。
看下Feed是如何实现2个功能的:
- 订阅和取消订阅:
Feed.Subscribe()
,入参是一个通道,通常是有缓冲的,就算是无缓存也不会造成Feed阻塞,Feed会校验这个通道的类型和本Feed管理的事件类型是否一致,然后把通道保存下来,返回给订阅者一个Subscription
,可以通过它取消订阅和读取通道错误。 - 发布事件和传递事件。
Feed.Send()
入参是一个事件,加锁确保本类型事件只有一个发送协程正在进行,然后校验事件类型是否匹配,Feed会尝试给每个订阅者发送事件,如果订阅者阻塞,Feed就继续尝试给下一个订阅者发送,直到给每个订阅者发送事件,返回发送该事件的数量。
Feed源码速递
Feed定义:
1 | // Feed implements one-to-many subscriptions where the carrier of events is a channel. |
订阅事件:
1 | // Subscribe adds a channel to the feed. Future sends will be delivered on the channel |
发送和传递事件:这个发送是比较绕一点的,要想真正掌握其中的运行,最好写个小程序练习下。
1 | // Send delivers to all subscribed channels simultaneously. |
- 如果这篇文章对你有帮助,不妨关注下我的Github,有文章会收到通知。
- 本文作者:大彬
- 如果喜欢本文,随意转载,但请保留此原文链接:http://lessisbetter.site/2018/10/18/ethereum-code-event-framework/