RDBMS - Race Condition 介紹及解決思路

今天紀錄有關於 RDBMS 方面會遇到的 Race Condition,參考 [TritonHo 大神的簡報](https://github.com/TritonHo/slides/tree/master/Taipei 2019-04 course),其實我很想去上他的收費課程,這個簡報是收費課程並開源的簡報真的很佛心 QQ,之前他開的免費 RESTful 淺談很實用,之後也要寫文章來紀錄一下 RESTful。

Race Condition 是什麼

簡單來說,就是至少兩個以上 Process 運行的途中,同時進入 Critical Section,而在這 Critical Section 可能對同一份資料進行更動或存取,遭成這種錯誤等現象。當然如果以 OS 的角度來看 Race Condition 會有更嚴謹的定義,這邊只針對在操作 RDBMS 可能會遇到的 Race Condition 的情況。

例子

今天假設有一個航班購票系統,每個人付錢去買座位,有可能會造成以下 Race Condition 的情境:

  1. Step 1A:使用者 A 查看 X 座位,這時系統顯示該座位是空的
  2. Step 1B:使用者B查看 X 座位,這時系統顯示該座位是空的
  3. Step 2:使用者 A 跟 B 同時決定要訂該座位,而使用者 B 先按下確定發送 Request
  4. Step 3:使用者 A 比 B 晚了幾秒後,也按下確定並發送 Request
  5. Step 4:使用者 B 的頁面顯示成功拿到座位
  6. Step 5:使用者 A 的頁面也顯示成功拿到座位,並且蓋過了使用者 B 成功買到座位的紀錄
  7. Step 6:使用者 B 到了現場時才發現沒了座位,開始怒告公司…

根據以上的流程我們可以知道,如果我們沒有針對可能會造成 Race Condition 的情境去做控管,就會導致使用者 B 的下場。

Race Condition 的必需條件

根據以上的例子我們可以知道:

  • 同一資料被同時改動
  • 檢查和資料改動不是在同一個 Atomic Operation 中完成

導致 Race Condition 的情況發生。

解決方式

根據以上的例子我們可以採用以下三種方式來解決:

  1. 讓同一航班的會改動的所有資料先拿到該航班的 Write Lock,當改動完成後才釋放 Write Lock 出去。

    例子:Redis、Zookeeper

    缺點:得到 Write Lock 的伺服器當掉的話就要等待 Write Lock 自然到期才可更新資料

  2. 為每一航班建立對應的 Queue,對於每個航班的改動必須先把工作放到對應的 Queue 然後以單線程順序執行

    可以想成每一個航班都有對應的工人對於訂票的工作會一個接一個去完成。

    缺點:需要類似 RabbitMQ Server、Worker Server、Server Push 等功能要控制會增加系統複雜度,通常這情境也滿適合用在聊天室。但是如果是轉帳的情境會卡在一個問題是工作要把在 A 的 Queue 還是 B 的 Queue。

  3. 利用 RDBMS 的 Atomicity and Isolation

    其實就是 check-and-set 的機制:

    1
    UPDATE seats set user = $user WHERE flight_no = 'BR855' and flight_date = '16SEP' and user = null 

    當成功執行之後,可以透過讀取回傳過來的 AffectedRowCount 就知道是否更新成功。之所以可以這樣是因為 RDBMS 支援原子性的 check-and-set,當使用者 B 先成功搶到座位,並把 Write Lock 交給使用者 A 的時候,在 Where 後面的 user = null 這個檢查就會是 false,導致 Update 的數量是 0,也就是沒搶到座位,也不會將使用者 B 的紀錄蓋掉。

    對於中小型的系統這樣的作法是最安全的也沒有什麼缺點。

總結

根據不同情境可以以這三個解決方法去抉擇,基本上大部分一定是用 RDBMS 的 Atomicity and Isolation 的機制來解決 Race Condition 的問題,下篇文章帶來紀錄 RDBMS 的多個 read phenomena,根據這些現象去呈現例子造成 Race Condition 的現象。