概述
訊息佇列相信大家都不陌生,平時在一些需要解耦、高併發的場景下經常能看見它們的身影,Kafka、RabbitMQ 這些常用的訊息佇列現在甚至已經成為後端程式設計師的必需技能了。那麼一個訊息佇列的基礎功能有哪些,是如何實現的?現在我們就用 Go 語言來實現一個簡單的單機版訊息佇列,藉以瞭解訊息佇列的原理。
這個訊息佇列的實現參考了 Go 語言中最受歡迎的訊息佇列 nsq 的實現,取名就叫 smq (simple message queue),程式碼已經上傳到 Github 上:https://github.com/yhao1206/SMQ。為了儘可能簡潔明瞭,這個訊息佇列的功能都很基礎。由於時間緊迫,加上本人也是 Go 語言的菜鳥,水平有限,有什麼錯誤或是建議,歡迎一起討論。
主要元件
- topic:一個 topic 就是程式釋出訊息的一個邏輯鍵,當程式第一次釋出訊息時就會建立 topic。
- channel:channel 與消費者相關,每當一個釋出者傳送一條訊息到一個 topic,訊息會被複制到 topic 下面所有的 channel 上,消費者透過 channel 讀取訊息。同一個 channel 可以由多個消費者同時連線,以達到負載均衡的效果。
- message:s訊息是資料流的抽象,消費者可以選擇結束訊息,表明它們已被正常處理,或者重新將它們排隊待到後面再進行處理。
- smqd:smqd 是一個守護程式,負責接收,排隊,投遞訊息給客戶端。
topic、channel、consumer 之間的關係可以參考下圖:
話不多說,讓我們直入主題,首先第一步就是定義訊息。
訊息
我們定義的訊息就是普通的位元組陣列,前 16 位是 uuid,用作訊息的唯一標識,後面完成和重排訊息時需要用到。後面就是訊息本身的內容,還提供了幾個匯出的封裝方法。
package message
type Message struct {
data []byte
}
func NewMessage(data []byte) *Message {
return &Message{
data: data,
}
}
func (m *Message) Uuid() []byte {
return m.data[:16]
}
func (m *Message) Body() []byte {
return m.data[16:]
}
func (m *Message) Data() []byte {
return m.data
}
工具庫
訊息體需要 uuid 作為唯一標識,那麼我們需要一個 uuid 生成器,我們直接使用一個工廠,不斷地朝一個 chan 中寫入uuid,程式碼如下:
package util
import (
"crypto/rand"
"fmt"
"io"
"log"
)
var UuidChan = make(chan []byte, 1000)
func UuidFactory() {
for {
UuidChan <- uuid()
}
}
func uuid() []byte {
b := make([]byte, 16)
_, err := io.ReadFull(rand.Reader, b)
if err != nil {
log.Fatal(err)
}
return b
}
func UuidToStr(b []byte) string {
return fmt.Sprintf("%x-%x-%x-%x-%x", b[:4], b[4:6], b[6:8], b[8:10], b[10:])
}
另外,由於 channel 和 topic 的實現中需要大量使用 Go 語言中的管道,甚至需要兩個 goroutine 之間透過同一個管道來傳遞資訊,多以我們事先定義好一個結構體方便後續 goroutine 之間通訊:
package util
type ChanReq struct {
Variable interface{}
RetChan chan interface{}
}
type ChanRet struct {
Err error
Variable interface{}
}
由於本文只是開篇,內容比較簡單,只是一些“地基”,不過我們後續會在這塊地基上添磚加瓦,完成一個可用的訊息佇列。
專案地址:https://github.com/yhao1206/SMQ
相關閱讀: