Quantcast
Channel: 小惡魔 – 電腦技術 – 工作筆記 – AppleBOY
Viewing all articles
Browse latest Browse all 325

用 Go 語言解決 MongoDB Transaction 機制

$
0
0
Screen Shot 2018-03-10 at 3.22.59 PM MongoDB 是一套具有高效能讀寫的 NoSQL 資料庫,但是不像傳統關連式資料庫,有非常好用的 Transaction 交易模式,而在 MongoDB 也可以透過 Two Phase Commits 來達成交易功能,大家可以先打開文件看看,非常冗長,工程師需要花很多時間閱讀文件並且實現出來。而在 Go 語言內,我們可以在 Single Thread 內同一時間點讀寫存取同一筆資料庫來解決此問題。此篇會透過 Go 語言方式來實現交易機制。

問題描述

底下步驟來產生資料
  1. 建立使用者,並且初始化每人 $1000 USD
  2. 接到新的交易請求
  3. 讀取使用者帳戶剩餘存款
  4. 將該帳號增加 $50 USD
根據上述的需求,我們可以知道,當有 100 個連線交易時,理論上該使用者的存款會變成 $1000 + $50*100 = $6000 USD。這是理想狀態,假設如果同時間打上來,大家可以知道最後存款肯定不到 $6000。底下程式碼可以複製出此問題
func main() {
    session, _ := mgo.Dial("localhost:27017")
    globalDB = session.DB("queue")
    globalDB.C("bank").DropCollection()

    user := currency{Account: account, Amount: 1000.00, Code: "USD"}
    err := globalDB.C("bank").Insert(&user)

    if err != nil {
        panic("insert error")
    }

    log.Println("Listen server on 8000 port")
    http.HandleFunc("/", pay)
    http.ListenAndServe(":8000", nil)
}
上述是主程式,新增一個 Handle 為 pay,用來處理交易。
func pay(w http.ResponseWriter, r *http.Request) {
    entry := currency{}
    // step 1: get current amount
    err := globalDB.C("bank").Find(bson.M{"account": account}).One(&entry)

    if err != nil {
        panic(err)
    }

    wait := Random(1, 100)
    time.Sleep(time.Duration(wait) * time.Millisecond)

    //step 3: subtract current balance and update back to database
    entry.Amount = entry.Amount + 50.000
    err = globalDB.C("bank").UpdateId(entry.ID, &entry)

    if err != nil {
        panic("update error")
    }

    fmt.Printf("%+v\n", entry)

    io.WriteString(w, "ok")
}

解決方式

這邊提供幾個解決方式,第一種就是透過 sync.Mutex 方式,直接將交易區段程式碼 lock 住,這樣可以避免同時寫入或讀出的問題。在 Handler 內直接新增底下程式碼就可以解決,詳細程式碼請參考 safe.go
    mu.Lock()
    defer mu.Unlock()
第二種方式可以用 Go 語言內的優勢: goroutine + channel,在這邊我們只要建立兩個 Channle,第一個是使用者帳號 (string) 第二個是輸出 Result (struct)。完整程式碼範例
    in = make(chan string)
    out = make(chan Result)
在 main func 內建立第一個 goroutine
    go func(in *chan string) {
        for {
            select {
            case account := <-*in:
                entry := currency{}
                // step 1: get current amount
                err := globalDB.C("bank").Find(bson.M{"account": account}).One(&entry)

                if err != nil {
                    panic(err)
                }

                //step 3: subtract current balance and update back to database
                entry.Amount = entry.Amount + 50.000
                err = globalDB.C("bank").UpdateId(entry.ID, &entry)

                if err != nil {
                    panic("update error")
                }

                out <- Result{
                    Account: account,
                    Result:  entry.Amount,
                }
            }
        }

    }(&in)
上面可以很清楚看到使用到 select 來接受 input channel,並且透過 go 將 for loop 丟到背景執行。所以在每個交易時,將帳號丟到 in channel 內,就可以開始進行交易,同時間並不會有其他交易。在 handler 內,也是透過此方式來讀取使用者最後存款餘額
    wg := sync.WaitGroup{}
    wg.Add(1)

    go func(wg *sync.WaitGroup) {
        in <- account
        for {
            select {
            case result := <-out:
                fmt.Printf("%+v\n", result)
                wg.Done()
                return
            }
        }
    }(&wg)

    wg.Wait()
不過上面這方法,可想而知,只有一個 Queue 幫忙處理交易資料,那假設有幾百萬個交易要同時進行呢,該如何消化更多的交易,就要將上面程式碼改成 Multiple Queue 完整程式碼範例。假設我們有 100 個帳號,開 10 個 Queue 去處理,每一個 Queue 來處理 10 個帳號,也就是說 ID 為 23 號的分給第 3 (23 % 10) 個 Queue,ID 為 59 號則分給第 9 個 Queue。
    for i := range in {
        go func(in *chan string, i int) {
            for {
                select {
                case account := <-*in:
                    out <- Result{
                        Account: account,
                        Result:  entry.Amount,
                    }
                }
            }

        }(&in, i)
    }
其中 channel 要宣告為底下: maxThread 為 10 (可以由開發者任意設定)
    in = make([]chan string, maxThread)
    out = make([]chan Result, maxThread)

效能測試

上述提供了三個解決方式,但是該選擇哪一種會比較好呢,底下是透過 [vegeta] http 效能檢測工具來實驗看看,底下先整理三種方法
  1. 使用 sync.Mutex
  2. 使用 single queue
  3. 使用 multiple queue
直接給數據看看
max Latencies mean Latencies
sync lock 25.558340237s 12.72966531s
single queue 672.252801ms 160.43181ms
multiple queue 476.134408ms 132.990084ms
可以看出來透過 multiple queue 測試效能是最好的,這數據是每秒打 1000 req,持續打 60 秒。

結論

這邊提供了三種解決方案,也許不是最好,如果能有更好的方式來解決會是更好,詳細的程式碼都有放在 go-transaction-example,歡迎大家拿去測試看看。

Viewing all articles
Browse latest Browse all 325