最近正在開發的一個Rails應用,由于后臺任務操作的AR對象存在競爭引發了issue,在本文中,我將使用悲觀鎖(pessimistic lock)來解決此問題。
舉個例子
首先,假設一個如下有feature的Rails應用:
- 管理員能看到客戶列表
- 管理員能訪問客戶主頁以及給客戶一條短信
- 管理員一天對一個客戶至多只能發送一條短信
為了讓情況盡可能簡單,假設一個Client模型和一條屬性last_sms_sent_on ,以及一個類(被用于controller或者Sidekiq任務):
class ClientSMSSender
def self.perform(client, message)
client.transaction do
if client.last_sms_sent_on.blank? || !client.last_sms_sent_on.today?
client.last_sms_sent_on = Date.today
client.save
SMSGateway.send(client.phone_number, message)
end
end
end
end
分析競爭問題
假設這時候出現其他情況,SMSGateway.send出現卡頓并且持續時間30秒以上。同時,其他管理員發起了一個新的請求并且發送SMS給客戶。由于競爭機制,我們將在同一天發送多條短信給客戶。
具體情況如下:
- 管理員A發起一個請求(Request A),發送一條SMS給客戶
- 客戶端幾天還沒有接收到任何短信
- Request A由于外部因素卡住
- 管理員B此時也發起一個請求并且發送一條短信給客戶
- 客戶今天還沒有收到任何短信(因為A卡主了)
- Request B同樣由于外部原因卡主
- Request A完成并且更新了client.last_sms_sent_on
- Request B 還是堅持之前的客戶端狀態
- Request B 完成,并且發送短信和更新狀態
解決方案
為了解決此問題,我們可以使用with_lock方法(ActiveRecord::Locking::Pessimistic)
代碼如下:
class ClientSMSSender
def self.perform(client, message)
client.with_lock do
if client.last_sms_send_on.blank? || !client.last_sms_sent_on.today?
client.last_sms_sent_on = Date.today
client.save
SMSGateway.send(client.phone_number, message)
end
end
end
end
with_lock:
- 開啟一個數據庫事務
- 重載數據記錄(為了獲取最新的記錄)
- 對此條記錄開啟一個互斥鎖
當使用with_lock之后:
- 管理員A發起一個請求(Request A),發送一條短信給客戶
- Request A 在數據庫端鎖住了這條記錄
- 客戶端幾天還沒有接收到任何短信
- Request A由于外部因素卡住
- 管理員B此時也發起一個請求并且發送一條短信給客戶
- 由于此條記錄被Request A鎖住,所以Request B一直掛起,直到數據庫釋放這條記錄。
- Request A完成并且更新了client.last_sms_sent_on
- 數據庫釋放(release)了這條記錄
- Request B (正在掛起,等待數據庫)開始執行。
- Request B 鎖住這條記錄
- Request B 重載數據(為了獲得最新數據)
- 客戶已經接受了一條數據
- Request B 完成,沒有發送短信
如果你對此還有疑惑,可以在Rails console中測試:
- 開啟2個Rails console
- 在第1個console中:
u = User.first
u.with_lock do
u.email = "test@thefrontiergroup.com.au"
u.save
sleep 40
end
在第二個console中:
u = User.first
u.email = "test2@thefrontiergroup.com.au"
u.save
第二個console中的u.save一直掛住直到第一個console完成了整個進程。此外最好不要將整個app鎖住,不然會遇到意想不到的麻煩。
總結
with_lock方法十分方便,但是盡量少使用。多出使用此方法雖然可以帶來好的邏輯持續性,但隨之而來的是巨大的性能消耗。
http://blog.thefrontiergroup.com.au/2015/11/handling-race-conditions-rails-pessimistic-locking/