話說,有這樣一個場景,就是客戶送不斷髮送訊息,需要服務端非同步處理。
一個一個的處理未免有些浪費資源,更好的方法是批次處理。
當訊息量特別大時,使用kafka之類的message queue自然是首選,但更多的時候,我們想用更加輕量的方案來解決這個問題。
下面來詳細分析一下技術需求,這個方案需要實現以下幾點:
- 訊息聚合後處理(最大條數為BatchSize)
- 延遲處理(延遲時間為LingerTime)
- 自定義錯誤處理
- 併發處理
基於這樣的需求,我快速的實現了第一步,訊息聚合後處理。
var (
eventQueue = make(chan interface{}, 4)
batchSize = 8
workers = 2
batchProcessor = func(messages []interface{}) {
fmt.Printf("%+v \n", messages)
}
)
for i := 0; i < workers; i++ {
go func() {
var batch []interface{}
for {
msg := <-eventQueue
batch = append(batch, msg)
if len(batch) == batchSize {
batchProcessor(batch)
batch = make([]interface{}, 0)
}
}
}()
}
for i := 0; i < 100; i++ {
eventQueue <- i
}
程式碼雖然簡單,但是核心已經有了。
- 帶buffer的channel相當於一個FIFO的佇列
- 多個常駐的goroutine來提高併發
- goroutine之間是並行的,但每個goroutine內是序列的,所以對batch操作是不用加鎖的。
下一步就是新增延遲處理,和錯誤處理了。
var (
eventQueue = make(chan interface{}, 4)
batchSize = 8
workers = 2
lingerTime = 14 * time.Millisecond
batchProcessor = func(batch []interface{}) error {
fmt.Printf("%+v \n", batch)
return nil
}
errHandler = func(err error, batch []interface{}) {
fmt.Println("some error happens")
}
)
for i := 0; i < workers; i++ {
go func() {
var batch []interface{}
lingerTimer := time.NewTimer(0)
if !lingerTimer.Stop() {
<-lingerTimer.C
}
defer lingerTimer.Stop()
for {
select {
case msg := <-eventQueue:
batch = append(batch, msg)
if len(batch) != batchSize {
if len(batch) == 1 {
lingerTimer.Reset(lingerTime)
}
break
}
if err := batchProcessor(batch); err != nil {
errHandler(err, batch)
}
if !lingerTimer.Stop() {
<-lingerTimer.C
}
batch = make([]interface{}, 0)
case <-lingerTimer.C:
if err := batchProcessor(batch); err != nil {
errHandler(err, batch)
}
batch = make([]interface{}, 0)
}
}
}()
}
for i := 0; i < 100; i++ {
eventQueue <- i
time.Sleep(1 * time.Millisecond)
}
雖然只多加了兩個點,程式碼明顯複雜了許多,這其實也是很多庫的成長過程吧。
一開始專注解決核心問題時,程式碼還很清晰,當功能逐漸擴充套件後,程式碼行數快速增加。
這時,如果抓不住核心,很容易迷失在程式碼中。關於這一點,相信大家在加入一個新的專案,或者看一些成熟專案的原始碼時都有同感。(這也是為什麼我把不同階段的程式碼都列出來的原因,不知各位看官意下如何)
言歸正傳,關於程式碼中為什麼使用time.Timer而不是time.After,是因為time.After在for select中使用時,會發生記憶體洩露。
具體分析,請檢視golang time.After記憶體洩露問題分析和GOLANG中time.After釋放的問題。
所以說呀,程式碼寫的越多,越容易出bug,但是功能不完善,程式碼還是要寫的。
實現到這裡,當個原型是綽綽有餘了,但是要作為一個通用的庫,還有很多功能要做,比如說:自定義配置。
最終版的程式碼,不多不少,正好200行,就不貼過來。有興趣的同學,請點選Aggregator.go檢視。
最後,Aggregator收錄在我開源的channelx倉庫中,這個庫目的是使用channel實現各種好用的輕量級工具。如果有你喜歡用的工具,歡迎點個贊或者star :)
本作品採用《CC 協議》,轉載必須註明作者和本文連結