Contents

資料庫的同步

網路上查到簡單說明transaction的例子,常常都是用轉賬來說明,但其實還有很多東西需要注意

帳戶轉賬,因為race condition的緣故,導致操作欄位的值被改錯。

  • A有100,B有40。

  • A轉100元給B,B轉20元給A,由兩個不同的client來處理操作。

  • 順序正確時:A會是減掉80

  • 但是當順序不正確,則數字會錯掉,例如A很早取得現在的餘額(100),但是更新數字0慢了,導致A最終變成0元,因為在B轉完帳後才更新值。

以上的這個問題很明顯的是race condition。所謂的race condition是結果必須依賴與某種特定的順序,但是順序又並不是固定的,因此偶爾會出現錯誤。

解決race condition的方法

為了要解決race condition,必須得要讓操作依照我們期望的順序才操作,才能預防不對的順序導致的錯誤。為了要達到這個目標,資料庫本身有提供兩種方法:

  1. atomic update:資料庫提供的一個,對於單一欄位數值做相依於原本值的操作指令。這個操作過程會是atomic的,也就是不會操作到一半值被改掉而導致結果出錯。

    1
    
    update products set quantity = quantity -1 where id = 1;
    
    • 優點:因為只是對於欄位做簡單的數字運算並馬上更新上去,所以對於欄位的佔用時間相較下面的select for update較短,且資料庫會將這類atomic操作做優化,所以效能會比較好。

    • 缺點:只支援對於數值的簡單操作後更新,並不能做太複雜的運算。

  2. select for update:這個操作被限制在transaction開起來的時候才能使用,使用時實際上就是將指定的欄位上一個更新的lock,此時直到commit之前,沒有其他client可以對這個欄位做任何更新。所以就能夠限制欄位更新的順序了。

    1
    2
    3
    4
    
    Begin;
    select quantity into $q from products where id = 1 for update;
    update products set quantity = $q-1;
    Commit;
    
    • 優點:因為有上鎖了,所以幾乎可以用各種邏輯運算來更新欄位的值。

    • 缺點:因為會在欄位上面上鎖,並且持續到commit位置,所以對於效能會有很大的影響。

  3. version scheme :在table上面多新增一個欄位叫做version,來確定現在更新時的version,是否從拿值的時候到現在還沒有被更新過。程式可以藉由資料庫回傳的訊息,是否有更改到任何row來判斷是否更新成功。

    1
    2
    
    old_ver = `select money, version from bank where name = A`
    `update bank set money = 30, version = version + 1 where name = A and version = old_ver`
    
    • 優點:能夠不使用任何鎖的時候實現。(或者還有其他我想不到的優點)

    • 缺點:可能會很浪費時間,尤其是當更新的頻率特別高的時候,可能很難可以得到把值更新下去的機會。並且失敗後得要重頭來過。可能會造成鎖的飢餓效應。

一開始提到的例子裡面真正用到transaction功能的地方

  • 利用transaction遇到錯誤時會rollback的特性,來在餘額小於0的時候,復原到還未轉賬之前的狀態。

  • 如何檢查餘額小於0,並在此時丟出錯誤呢?

    1. 利用select for update鎖住欄位更新,在此時檢查,通過且做完操作後commit,釋放鎖。就可以避免此時有其他操作更動到這個帳戶的餘額。

      • 優點:可以實作各種複雜的檢查邏輯

      • 缺點:必須為這個帳戶的餘額上鎖,此時沒有其他人可以操作(也就是沒有其他人可以轉賬給他、領錢等操作),會導致效能低下。

    2. 利用check constraint 來讓資料庫在欄位不符合規範的時候丟出錯誤。此時就可以配合transaction來rollback復原了。

      • 優點:因為check constraint是資料庫提供可以給欄位增加規範的設定,檢查會在更新的時候做,所以即使背後有使用鎖,所花費的時間會比用select for update還要少。

      • 缺點:只能依照資料庫提供的有限constraint來設定,並不一定能夠做到想要的規範。

結論

  • 遇到需要注意順序的操作,先試著能不能使用atomic update的方式來完成需求,沒有辦法的時候才會使用select for update的方式來限制進到這個transaction後,必須要操作完這邊的指令後,才能換別人改值。

  • 遇到需要檢查數值符不符合規範,先試著用check constraint來限制欄位,真的沒有辦法,才會採用select for update的方式來將欄位上鎖,來做檢查。

參考資料

How to avoid the race condition and the negative value

check constraint

select for update 詳解

check constraint