PubSub(Publish/Subscribe)模式,,意為“釋出/訂閱”模式,是為了解決一對多的依賴關係,使多個消費者同時監聽某一個主題,不僅可以讓生產者和消費者解耦,同時也讓不同的消費者之間相互解耦(注:有些反模式依賴訂閱者執行的先後順序,使用共享資料來傳遞狀態,是需要避免的,因為這樣會使消費者耦合在一起,不能獨立變化)。這其中的關鍵就在於需要有中介來維護訂閱關係,並負責把生產的訊息,傳遞給訂閱方。
在Golang這門語言中,channel天然就適合來當這個中介,下面就讓我們一步步根據PubSub模式,實現工具類EventBus.
首先,讓我們先定義一些基本型別和核心操作。
//EventID是Event的唯一標識
type EventID int64
//Event
type Event interface {
ID() EventID
}
//EventHandler
type EventHandler interface {
OnEvent(ctx context.Context, event Event) error
CanAutoRetry(err error) bool
}
// JobStatus holds information related to a job status
type JobStatus struct {
RunAt time.Time
FinishedAt time.Time
Err error
}
//EventBus ...
type EventBus struct {}
func (eb *EventBus) Subscribe(eventID EventID, handlers ...EventHandler) { }
func (eb *EventBus) Unsubscribe(eventID EventID, handlers ...EventHandler) { }
func (eb *EventBus) Publish(evt Event) <-chan JobStatus { }
首先,消費者要透過Subscribe來訂閱相關的主題,這其中的重點就是需要根據EventID維護訂閱的消費者,很自然的想到map,我們選擇用handlers map[EventID][]EventHandler
來維護,考慮到併發問題,還需要來加個鎖。
//Subscribe ...
func (eb *EventBus) Subscribe(eventID EventID, handlers ...EventHandler) {
eb.mu.Lock()
defer eb.mu.Unlock()
eb.handlers[eventID] = append(eb.handlers[eventID], handlers...)
}
這裡實現的比較簡單,沒有考慮一個消費者,重複訂閱的問題,留給了使用方自己處理。(但同一個消費者為什麼要多次呼叫subcribe,訂閱同一個主題呢,感覺是在寫bug)
下面就是最核心的Publish函式了,一方面一定是需要一個channel(最好是有buffer的)來傳遞Event資料,另一方面,為了保證效能,需要有一些常駐協程,來監聽訊息,並啟動相關的消費者。以下是相關程式碼(在完整版程式碼裡,新增了日誌、錯誤處理等,這裡為了展示重點,暫且隱去)
func (eb *EventBus) Start() {
if eb.started {
return
}
for i := 0; i < eb.eventWorkers; i++ {
eb.wg.Add(1)
go eb.eventWorker(eb.eventJobQueue)
}
eb.started = true
}
func (eb *EventBus) eventWorker(jobQueue <-chan EventJob) {
loop:
for {
select {
case job := <-jobQueue:
jobStatus := JobStatus{
RunAt: time.Now(),
}
ctx, cancel := context.WithTimeout(context.Background(), eb.timeout)
g, _ := errgroup.WithContext(ctx)
for index := range job.handlers {
handler := job.handlers[index]
g.Go(func() error {
return eb.runHandler(ctx, handler, job.event)
})
}
jobStatus.Err = g.Wait()
jobStatus.FinishedAt = time.Now()
select {
case job.resultChan <- jobStatus:
default:
}
cancel()
}
}
}
做好上面的準備工作後,以下就是真正的Publish程式碼了。
// EventJob ...
type EventJob struct {
event Event
handlers []EventHandler
resultChan chan JobStatus
}
//Publish ...
func (eb *EventBus) Publish(evt Event) <-chan JobStatus {
eb.mu.RLock()
defer eb.mu.RUnlock()
if ehs, ok := eb.handlers[evt.ID()]; ok {
handlers := make([]EventHandler, len(ehs))
copy(handlers, ehs) //snapshot一份當時的消費者
job := EventJob{
event: evt,
handlers: handlers,
resultChan: make(chan JobStatus, 1),
}
var jobQueue = eb.eventJobQueue
select {
case jobQueue <- job:
default:
}
return job.resultChan
} else {
err := fmt.Errorf("no handlers for event(%d)", evt.ID())
resultChan := make(chan JobStatus, 1)
resultChan <- JobStatus{
Err: err,
}
return resultChan
}
}
這裡沒有在eventWorker中直接從handlers中根據ID拿到相關的消費者,一方面是為了讓eventWorker更加通用,另一方面也是為減少因為鎖操作引起的阻塞。
至此,我們已經把最核心的程式碼一一拆解完成,完整程式碼,請參見channelx專案中的event_bus.go
沒有例子的工具類是不完整的,下面就提供一個例子。
先定義一個event,這裡把id
定義成私有的,然後在建構函式中,強制指定。
const ExampleEventID channelx.EventID = 1
type ExampleEvent struct {
id channelx.EventID
}
func NewExampleEvent() ExampleEvent {
return ExampleEvent{id:ExampleEventID}
}
func (evt ExampleEvent) ID() channelx.EventID {
return evt.id
}
接下來是event handler,需要根據實際的需要,在OnEvent
中檢查接收到的事件是否是訂閱的事件,以及接收到事件結構是否能轉換成特定的型別。在防禦程式設計後,就可以處理事件邏輯了。
type ExampleHandler struct {
logger channelx.Logger
}
func NewExampleHandler(logger channelx.Logger) *ExampleHandler {
return &ExampleHandler{
logger: logger,
}
}
func (h ExampleHandler) Logger() channelx.Logger{
return h.logger
}
func (h ExampleHandler) CanAutoRetry(err error) bool {
return false
}
func (h ExampleHandler) OnEvent(ctx context.Context, event channelx.Event) error {
if event.ID() != ExampleEventID {
return fmt.Errorf("subscribe wrong event(%d)", event.ID())
}
_, ok := event.(ExampleEvent)
if !ok {
return fmt.Errorf("failed to convert received event to ExampleEvent")
}
// handle the event here
h.Logger().Infof("event handled")
return nil
}
最後,就是EventBus
的啟動,事件的訂閱和釋出了。
eventBus := channelx.NewEventBus(logger, "test", 4,4,2, time.Second, 5 * time.Second)
eventBus.Start()
handler := NewExampleHandler(logger)
eventBus.Subscribe(ExampleEventID, handler)
eventBus.Publish(NewExampleEvent())
之前還寫過一些關於channel的使用的文章,
- 如何把Golang的channel用的如nodejs的stream一樣絲滑
- 如何用Golang的channel實現訊息的批次處理
- 如何把Golang的Channel玩出async和await的feel
- 下次想在Golang中寫個併發處理,就用這個模板,準沒錯!
裡面實現的輕量級util都開源在channelx,歡迎大家審閱,如果有你喜歡用的工具,歡迎點個贊或者star :)
本作品採用《CC 協議》,轉載必須註明作者和本文連結