Event Store 是一個基於 CQRS 與 Event Sourcing 理念所衍生出來的新概念並由 C# 撰寫。這是一個微服務事件儲藏中心,這可能很難懂,但別緊張,這些都會在本文中得到答案。

在微服務結構中,每個服務都是獨立的,這意味著沒有服務該依賴另一個服務(無相依性),那麼我們應該要怎麼在服務之間互相溝通?一但有了這個問題就會開始陷入死循環,最終甚至會做出一個比傳統式單體應用程式還要糟糕的結構。
首先你要知道的是微服務應該自主。需要什麼資料的時候就應該自己處理,而不是發送請求到另一個服務然後等待回應。
實際案例
讓我們假設你有個文章服務,這個服務會接收使用者帳號(Username)然後轉成使用者編號(User ID)才會繼續進行處理,那這個時候要怎麼辦?從文章服務呼叫使用者服務來幫我們進行轉換嗎?

不對。這樣一來微服務之間就會有相依性。我們要做的就是在文章服務和使用者服務各儲存一份使用者資料。然後我們就可以在各自的服務中直接進行轉換的動作。(先撇開這樣會有重複函式、跨服務理念的問題)
解決方案
現在問題來了,如果我們希望兩個服務都各有一份使用者資料,我們應該要怎麼處理?這問題很簡單,透過「事件廣播」,而這就是 Event Sourcing。

(圖片來源:http: //microservices.io/patterns/data/event-sourcing.html)
當新使用者註冊時,會發送請求到使用者服務,對吧?接著使用者服務會向 Event Store 廣播一個「新使用者註冊」事件,這個事件會參雜新使用者的資料。
接著我們的文章服務事先註冊了「新使用者註冊」事件,這意味著當有「新使用者註冊」事件發生的時候,文章服務就會知道。接著我們就能夠在文章服務中獲取這個新使用者的資料,然後存放在文章服務自己的資料庫內供日後轉換。
為什麼是 Event Store?
有個問題你可能沒想到,如果後來有一個新服務冒出來了,這個新服務要怎麼獲取先前那些註冊過的使用者資料?(這個問題我有在 StackOverflow 詢問)

這正是我們要使用 Event Store 的原因,傳統的 Event Sourcing 只會在微服務中傳遞訊息而沒有「紀錄(Store)」的功能。Event Store 解決了這個問題,你的所有廣播事件還有參帶的資料都會被記錄到 Event Store 中,簡單說就像是一個事件資料庫一樣,這也被稱為「事件儲藏中心」。
一但你有新的服務要上線時,你就可以在這個新服務中重播(Replay)先前的所有事件,那麼你就會擁有先前所有的使用者資料了。
1. 安裝與啟動 Event Store
先進入 Event Store 的下載頁面,接著選取符合你系統的壓縮或安裝檔。

接著解壓縮,然後在解壓縮的目錄直接執行 eventstored
檔案即可。
$ ./eventstored --db .\ESData
接著就能夠看到像下面這樣執行的介面。

2. 網頁管理介面
在剛才解壓縮的目錄中有個 clusternode-web
資料夾,裡面是 Event Store 的網頁管理介面。
透過瀏覽器開啟該資料夾內的 index.html
即能看到下列畫面。在這個畫面中你會需要輸入帳號密碼,預設即是 admin
與 changeit
。

登入之後就會來到後台管理畫面,從這裡就能夠管理或是直接將 Event Store 關機。

3. 引用套件庫
在這裡我們會用到 go.geteventstore 套件。在終端機中透過 go get
指令取得。
$ go get github.com/jetbasrawi/go.geteventstore
接著在你的程式中引用該套件。
import "github.com/jetbasrawi/go.geteventstore"
4. 連線到 Event Store
接著透過下列程式碼來開啟到 Event Store 的連線並配置帳號密碼。
// 配置連線
client, err := goes.NewClient(nil, "//你的EventStore網址:2113")
if err != nil {
log.Fatal(err)
}
// 配置帳號密碼設定
client.SetBasicAuth("admin", "changeit")
5. 在 Golang 中發送事件
現在我們要開始在 Golang 發送一個新的事件到 Event Store,一個事件帶有三個東西:名稱、內容、中繼資料。這些東西的作用分別是:
- 名稱:例如
new_user
、edit_user
等。 - 內容:事件所要夾帶的內容,假設是「新使用者事件」,那麼我們就可以在其中夾帶這個新使用者的內容,其他服務也就能從這個事件中獲取使用者資料。
- 中繼資料:小型的內容,例如哪個服務發送了這個事件、這個事件的權限資料、壽命或過期時間等。
實際範例如下:
// 建立事件內容。
myEvent := &FooEvent{
FooField: "Lorem Ipsum",
BarField: "Dolor Sit Amet",
BazField: 42,
}
// 建立中繼資料。
myEventMeta := make(map[string]string)
myEventMeta["Foo"] = "consectetur adipiscing elit"
// 將事件與中繼資料用 goes.Event 函式包覆著。
myGoesEvent := goes.NewEvent(goes.NewUUID(), "FooEvent", myEvent, myEventMeta)
// 建立一個串流撰寫者 StreamWriter,而 `FooStream` 是事件名稱。
writer := client.NewStreamWriter("FooStream")
// 將事件撰寫到該串流中,在這裡我們會把 expectedVersion 設為 nil,
// 因為我們不會有並發上資料重複的衝突。
err := writer.Append(nil, myGoesEvent)
if err != nil {
// 處理錯誤。
}
6. 讀取事件
讀取事件分為兩種,一種是「讀取全部」,另一種是「讀取最新端」。
- 讀取全部:這就像是重播一樣,你會接收到以往的所有事件,然後就結束。這意味著你並不會接收到在你之後所發生的事件。
- 讀取最新端:你不會接收到以前的事件,但是你會接收到在你之後的新事件,並且持續監聽。
讀取全部
這很適合讀取以前所發生的事件。
// 建立一個讀取 FooStream 事件的串流讀取者 goes.StreamReader。
reader := client.NewStreamReader("FooStream")
// 呼叫 Next 來取得下一個事件。
for reader.Next() {
// 檢查讀取的資料有沒有錯誤。
if reader.Err() != nil {
// 處理錯誤。
}
// 如果這份資料沒有問題,那麼我們就用一個符合這個事件資料形態
// 的建構體來解析這個事件的內容和中繼資料。
fooEvent := FooEvent{}
fooMeta := make(map[string]string)
// 呼叫 Scan 來將事件內容與中繼資料解析到該建構體中。
err := reader.Scan(&fooEvent, &fooMeta)
if err != nil {
// 在此處理解析錯誤。
}
}
讀取最新端
這會讓你持續地接收到最新的事件(在你啟動之後所發生的事件)。
reader := client.NewStreamReader("FooStream")
for reader.Next() {
if reader.Err() != nil {
// 當沒有新的事件時,我們就設置 LongPoll 來等待。
// 伺服器會每 15 秒重新檢查一次,或是等待串流裡有新的事件。
if e, ok := reader.Err().(*goes.ErrNoMoreEvents); ok {
reader.LongPoll(15)
}
} else {
fooEvent := FooEvent{}
_ := reader.Scan(&fooEvent, &fooMeta)
}
}