--- 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) ![](https://i.imgur.com/pL0vOzN.gif) ## 如何處理 將要進行操作的 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) ![](https://i.imgur.com/gNwpDiO.gif) ```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/) 備註:之後文章修改更新,以個人部落格為主