大家有想過銀行在做交易時,是如何確保金額不會算錯呢,如果同一個時間點,對帳戶存入錢並同時領出錢,金額會不會亂掉呢?


Transaction

實際上程式在操作涉及金額的運行的時候都會對任何Transaction使用ACID的機制。 Transaction在英文中可以翻譯成交易,但在資料庫語言中我們可以把Transaction理解成一組一連串對資料庫執行存取/讀取的行為。

transaction = 一組一連串對資料庫執行存取/讀取的行為

ACID

在理解Transaction後,我們可以來更進一步了解他的機制ACID。 ACID 是什麼?請解釋 ACID 特性文章中有提到,ACID的定義為:

  1. 原子性(Atomicity):事務是不可分割的完整個體,只有「全部執行」或者「全部不執行」兩個選項。
  2. 一致性(Consistency):這邊指的「一致」為資料庫的一致狀態,是指在事務開始的前後,資料庫的完整性沒有被破壞,表示寫入的資料必須完全符合所有預設的規範。
  3. 事務隔離(Isolation):某事務執行期間所用的資料或中間結果,不容許其他交易讀取或寫入,直到被 Commit 為止。
  4. 持久性(Durability):一旦交易 Commit 後,其對資料庫做的變更是永遠有效的,即使未來系統當機或損毀。

阿好複雜喔,[極短篇] 資料庫的 ACID 是什麼?有用更白話的說法來解釋ACID:

  1. 原子性(Atomicity):交易要馬成功要馬失敗,沒有成功一半的結果。
  2. 一致性(Consistency):交易前後狀態依然一樣,每筆Transaction影響後並不會破壞資料庫的完整性。
  3. 事務隔離(Isolation):每個Transaction不會被其他Transaction影響。
  4. 持久性(Durability):一旦Transaction完成,紀錄將永遠存在。

Atomicity 原子性

必須確保AB兩方都必須成功,如果失敗的話就要復原為上一步驟。 例如:

A 匯錢給 B,必須要執行以下兩個步驟
1. A.money -= 100
2. B.money += 100

Consistency 一致性

必須遵守一開始定義的規則,交易前後都需要遵守。 例如:

1. 雙方的錢都不能小於 0
2. 雙方錢的總和不能改變

Isolation 隔離性

多個交易不能互相干擾。 正常的情況為:

1. 小明跟小亮各有500,各匯100給戶頭有500的小美
2. 小明匯錢,小明戶頭500-100=400,之後小美拿到錢500+100=600
3. 小亮匯錢,小亮戶頭500-100=400,之後小美拿到錢500+100=700
4. 跟預期的一樣小美戶頭多了200

但如果不對交易實施隔離性會發生以下很恐怖的狀況:

1. 小明跟小亮各有500,同一時間匯100給戶頭有500的小美
2. 小明匯錢,小明戶頭500-100=400,由於小明與小亮交易同時進行,系統讀取到小美原戶頭為500,之後小美拿到錢500+100=600
3. 小亮匯錢,小亮戶頭500-100=400,由於小明與小亮交易同時進行,系統讀取到小美原戶頭為500,之後小美拿到錢500+100=600
4. 跟預期的不一樣小美戶頭只多了100

什麼!竟然憑空少了100,明明兩個人各匯了100,小美的錢應該要多200阿~ 這邊要牽扯到,軟體底層的運行機制:Racing Condition。

Race Condition

來看看維基百科怎麼敘述競爭危害

競爭危害(race hazard)又名競態條件、競爭條件(race condition),它旨在描述一個系統或者進程的輸出依賴於不受控制的事件出現順序或者出現時機。此詞源自於兩個訊號試著彼此競爭,來影響誰先輸出。

以銀行的例子來說,當兩個交易請求同時使用共享資料時,共享資料為小美的500塊戶頭,小明跟小亮的底層程式會同時取用小美的500塊,兩筆匯錢程序會處於競爭關係,誰先誰後無法確定,但能確定的是如果小明跟小亮同時交易,必定有一個人匯了100,但小美沒拿到錢。

Race Condition and Deadlock中提到了,“When two processes are competing with each other causing data corruption”, 意思為兩筆處理程序為了某個共享資源競爭造成資源損壞。

在程式的世界中,底層語言是這樣運營的,共享資料我們先把他稱為變數,變數是能用暫存空間儲存資料的一個方法。

  1. 讀取變數(共享資料)。
  2. 為變數增加值。
  3. 寫回變數。

由於第一步是要讀取變數,造成程序間彼此有資訊落差,從Race Condition and Deadlock舉的圖可以很更了解為什麼會發生Race Condition。如下圖所示, Person A 跟 Person B 都讀取了Bank的餘額17,之後他們同步進行交易處理程序,都不知道彼此已經做了17+1加錢的動作,最後一同把18塊的錢存回銀行,銀行竟然也不知道。

要避免這種狀況我們需要在程式碼裡面加點小魔法,讓我們來深入看看該怎麼做!

如何避免Racing Condition

[Golang] Goroutine Concurrency多執行緒淺談舉的例子非常好,同時取用金額的目標時為其中一個請求先上鎖lock.Lock(),代表現在現在只有我可以握有資源並使用,執行完後才會lock.Unlock(),代表我把資源釋出,其他人可以使用了。

package main

import (
        "fmt"
        "sync"
        "time"
)
//範例: 多個執行序讀寫同一個變數

func main() {
        var lock sync.Mutex // 宣告Lock 用以資源佔有與解鎖
        var wg sync.WaitGroup // 宣告WaitGroup 用以等待執行序
        val := 0
        // 執行 執行緒: 將變數val+1
        go func() {
                defer wg.Done() //wg 計數器-1
                //使用for迴圈將val+1
                for i := 0; i < 10; i++ {
                        lock.Lock()//佔有資源
                        val++
                        fmt.Printf("First gorutine val++ and val = %d\n", val)
                        lock.Unlock()//釋放資源
                        time.Sleep(3000)
                }
        }()     
        // 執行 執行緒: 將變數val+1
        go func() {
                defer wg.Done()//wg 計數器-1
                //使用for迴圈將val+1
                for i := 0; i < 10; i++ {
                        lock.Lock() //佔有資源
                        val++
                        fmt.Printf("Sec gorutine val++ and val = %d\n", val)
                        lock.Unlock()// 釋放資源
                        time.Sleep(1000)
                }
        }()
        wg.Add(2)//記數器+2
        wg.Wait()//等待計數器歸零
}

上面的程式碼會印出下圖,以銀行的例子來,確保從銀行取出的金額就算是同時取出也不會造成同時寫入相同金額給銀行,小明與小亮匯出錢的順序雖然不一定,但一定能確保不會造成匯給小美錢時發生金額錯誤的問題!

Durability 持久性

交易如果成功了,必定永遠存在,如果網路掛點,伺服器壞掉,交易也應該被永久的寫入裝置中,永遠存在。

重點回顧

  1. ACID是什麼?
  2. Racing Condition 是甚麼?
  3. 如何避免Racing Condition

總結

ACID為銀行進行金錢交易必定要做到的部分,Racing Condition是在ACID實作時必定要預防的問題,人在江湖走,防止transaction出錯的行情也要有!