玩轉Golang的channel,二百行程式碼實現PubSub模式

jeremy1127發表於2021-09-21

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的使用的文章,

裡面實現的輕量級util都開源在channelx,歡迎大家審閱,如果有你喜歡用的工具,歡迎點個贊或者star :)

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章