使用 Redis 作為分散式鎖用於簡單的用例
最近我遇到了一個系統問題,我們一些以 Ruby Rake 任務形式編寫的批次作業最終出現重複調用,這導致資料庫中出現重複記錄或在 Slack 中出現重複警報。這絕對是基礎設施問題,但在從基礎設施修復它之前,我需要從編碼角度找到解決它的方法。嘗試將所有內容重寫為盡可能冪等的並不太現實,增強應用程式級驗證或資料庫唯一性約束也不是一個選擇,因為並非所有作業都可以是冪等的,也並非所有作業都會寫入資料庫,此外問題是由競爭條件引起的,不是來自兩個線程,而是來自兩個進程。經過一些研究和實驗,我決定使用「Redis」作為一個簡單的分散鎖解決方案,它已經在我們的應用程式中可用,不需要任何額外的設置,而且它是一個單一實例,因此不需要考慮複製同步問題。 「Redis」 是單執行緒架構,無論有多少並發客戶端嘗試寫入它,它都能保證結果是順序的和原子的。我發現可以利用這種性質來解決所面臨的問題。
任務範例
namespace :main do
task :hello do
sleep 1 # simulate long-running task
puts '***hello, world***'
end
end
無鎖運行
$ bundle exec rake main:hello & bundle exec rake main:hello &
[1] 38358
[2] 38359
***hello, world***
***hello, world***
[2] + done bundle exec rake main:hello
[1] + done bundle exec rake main:hello
兩個進程同時運行,並且運行兩次。
新增redis分散式鎖
- 定義鎖
namespace :lock do
redis = Redis.new
task :before do
task_name = Rake.application.top_level_tasks[0]
lock_obtained = redis.set task_name, '1', ex: 60, nx: true
abort "Another process is already running #{task_name}" unless lock_obtained
end
task :after do
task_name = Rake.application.top_level_tasks[0]
redis.del task_name
end
end
- 任務加鎖
Rake::Task['main:hello'].enhance(['lock:before']) do
Rake::Task['lock:after'].execute
end
帶鎖運行
$ bundle exec rake main:hello & bundle exec rake main:hello &
[1] 38560
[2] 38561
Another process is already running main:hello
[1] - exit 1 bundle exec rake main:hello
***hello, world***
[2] + done bundle exec rake main:hello
兩個進程同時運行它,但它只運行一次。
概括
關鍵在於利用 set
指令的 nx: true
選項,它表示『僅當鍵不存在時才設定它』。利用redis的單執行緒設計,我們可以確保只有一個進程能夠成功設定key,也就是說只有一個進程獲得鎖。
作為參考,Distributed Locks with Redis 有更多關於如何實現更可靠、更安全的基於演算法的鎖(如 Redlock
)的詳細信息,適用於更複雜的情況。