---
title: Race Condition
date: 2022-05-25
categories: 公司內部技術分享
---
## 前言
[Race Condition](https://zh.wikipedia.org/wiki/競爭危害) 可翻譯成「競爭條件」,在中文版 [Wiki](https://zh.wikipedia.org/wiki/競爭危害) 上看不懂的話,可看英文版 [Wiki](https://en.wikipedia.org/wiki/Race_condition) 的描述,會比較清楚,以下為白話文翻譯:
> 同筆資料同時被 2 thread 以上操作,導致結果的不正確
### 常見情境可能有:
1. 搶票系統、搶購限量商品時 (ex: 限量 100 張票,卻賣了 101 張)
2. 使用者送出資料時,剛好這時 server 負載較重 (處理比較慢),使用者以為還沒處理完成,於是在前端連點,雖然 `model` 有做 `validates :email, uniqueness: true` ,但 DB 沒再次驗證,也有可能發生此問題 (可參考: [ActiveRecord - 資料驗證及回呼](https://ihower.tw/rails/activerecord-lifecycle.html))
---
## 如何重現 Race Condition
以 Ruby on Rails 為例,想看 Race Condition 本人的話
在 `rails console` 貼上以下這段 (本文以此 [repo 作為範例](https://github.com/River-Ye/ironman_12th_2020))
```ruby
# 重現 Race Condition
# 一開始 Order.last.total_price = 0
Order.last.update(total_price: 0)
threads = [100, 10].map do |n|
Thread.new do |_t|
order = Order.last
order.total_price += n
order.save
end
end
threads.each(&:join)
puts "預期結果是: 110, 實際結果是: #{Order.last.total_price}"
# 預期結果是: 110, 實際結果是: 10
# 預期結果是: 110, 實際結果是: 100
# 預期結果是: 110, 實際結果是: 110
# 上述每次執行,實際結果會不一樣
```
#### Race Condition 本人 (上述例子,每次執行,得到結果會不同)
[[Youtube] 用 Ruby on Rails 重現 Race Condition](https://youtu.be/k7379m2_56A)

## 如何處理
將要進行操作的 table 先鎖住 ([Lock](https://en.wikipedia.org/wiki/Lock_(computer_science))),處理方式可分成 2 種:
1. 悲觀鎖 (Pessimistic locking)
2. 樂觀鎖 (Optimistic locking)
### 悲觀鎖 (Pessimistic locking)
悲觀鎖,如其名,不相信任何人,一次只允許一筆資料針對 table 操作,此時會先鎖住該 table (鎖又可分成表鎖、行鎖,這邊以行鎖為例),避免被人竄改,其他人要操作只能等他被釋放後,才能進行操作
白話文就是所有人排隊領號碼牌,叫號依序處理,能解決 Race Condition,但也會影響效能,畢竟一次只能處理一筆資料
Ruby on Rails 中,悲觀鎖,可使用 `with_lock` 處理,實作方式如下:
```ruby
# 悲觀鎖
# 一開始 Order.last.price = 0
Order.last.update(total_price: 0)
threads = [100, 10].map do |n|
Thread.new do |_t|
order = Order.last
order.with_lock do
order.total_price += n
order.save
end
end
end
threads.each(&:join)
puts "預期結果是: 110, 實際結果是: #{Order.last.total_price}"
# 預期結果是: 110, 實際結果是: 110
```
上述確實解決了 Race Condition ,但變成其他人要排隊等待 (可看下方 GIF),使用悲觀鎖需在效能與資料正確性之間做取捨,可依問題產生嚴重性、衍伸損失等進行綜合評估決定是否使用
#### 排隊等待畫面
[[Youtube] 悲觀鎖 (Pessimistic locking),排隊等待畫面](https://youtu.be/6WMH5MfIdjM)

```ruby
# 悲觀鎖
# 示範如何鎖住 table (行鎖)
# console 1
Order.last.update(total_price: 0)
order = Order.last
order.with_lock do
puts "total_price is #{order.total_price}"
order.total_price += 10
byebug
order.save
end
# console 2
puts "total_price is #{Order.last.total_price}"
# 行鎖: 其他 Order 不受影響
Order.first.update(total_price: 0)
Order.last.increment!(:total_price)
# 此時應該會卡住,因為 console 1 with_lock 關係,需等 console 1 釋放 order 後, console 2 才能針對該筆資料進行操作
puts "預期結果是: 11, 實際結果是: #{Order.last.total_price}"
# 預期結果是: 11, 實際結果是: 11
```
### 樂觀鎖 (Optimistic locking)
與悲觀鎖意思相反,認為資料不會頻繁被操作,因此允多人針對 table 操作,不代表我就爛什麼都不管,在 Ruby on Rails 中有提供 `lock_version` 這方法,可加在想使用樂觀鎖的 table 上,可參考此 [commit](https://github.com/River-Ye/ironman_12th_2020/pull/11/commits/cd1853d168d84bc6098efd0148138911335e8e50)
Ruby on Rail 中,樂觀鎖,可使用 `lock_version` 處理,實作方式如下:
```ruby
# 樂觀鎖
# 一開始 Order.last.total_price = 0
Order.last.update(total_price: 0)
begin
order1 = Order.last
order2 = Order.last
order1.total_price += 10
order1.save
order2.total_price += 100
order2.save # ActiveRecord::StaleObjectError: Attempted to update a stale object: Order.
rescue ActiveRecord::StaleObjectError => e
# 要自己處理異常
end
puts "預期結果是: 10, 實際結果是: #{Order.last.reload.total_price}"
# 預期結果是: 10, 實際結果是: 10
```
[[Youtube] 樂觀鎖 (Optimistic locking)](https://youtu.be/EtrV-Xfg5NU)
樂觀鎖好處是能同時處理多筆資料,但錯誤的話,會收到 `ActiveRecord::StaleObjectError`,要自己處理,像是可以寫個 `retry` 或報錯誤訊息,讓工程師知道
## 參考資料
1. [Active Record 查詢 — Ruby on Rails 指南](https://rails.ruby.tw/active_record_querying.html#更新時鎖定記錄)
2. [樂觀鎖 與 悲觀鎖 Optimistic Locking & Pessimistic Locking](https://mgleon08.github.io/blog/2017/11/01/optimistic-locking-and-pessimistic-locking/)
3. [不使用 lock 又要避免 race condition,可能嗎?](https://khiav223577.github.io/blog/2019/02/07/不使用-lock-又要避免-race-condition,可能嗎?/)
4. [Race Conditions on Rails](https://karolgalanciak.com/blog/2020/06/07/race-conditions-on-rails/)
5. [Rails 中避免 race condition 的最佳實踐(一)](https://blog.niclin.tw/2020/09/11/avoids-race-condition-best-practice-in-ruby-on-rails-1/)
6. [Rails 中避免 race condition 的最佳實踐(一)](https://blog.niclin.tw/2020/09/11/avoids-race-condition-best-practice-in-ruby-on-rails-2/)
---
## 小結
解決 Race Condition 後,需留意是否可能衍伸另個問題,像是 Deadlock 可看 Wiki [哲學家就餐問題](https://zh.wikipedia.org/wiki/哲學家就餐問題) 這篇,推薦看上方參考資料,可看看不同大大們對於 Race Condition 的介紹與解法
本篇特別感謝 David 、 Johnson(詹昇) 協助 (依英文字母順序排列)
---
鐵人賽文章連結:[https://ithelp.ithome.com.tw/articles/10244812](https://ithelp.ithome.com.tw/articles/10244812)
medium 文章連結:[https://link.medium.com/AUCVQnUb69](https://link.medium.com/AUCVQnUb69)
本文同步發布於 [小菜的 Blog](https://riverye.com/2020/09/27/Day22-Ruby-on-Rails-中的-Race-Condition/) [https://riverye.com/](https://riverye.com/)
備註:之後文章修改更新,以個人部落格為主