- 気軽に使うのはおすすめできません
with_lockの挙動
RDBの SELECT FOR UPDATE
を使って実現されています
https://api.rubyonrails.org/classes/ActiveRecord/Locking/Pessimistic.html
危険な使い方
- current_userのようなスコープが広いレコードをlockしてしまう
class HogeController
def action_hoge
current_user.with_lock do
hoge_logic
end
end
end
class FugaController
def action_fuga
current_user.with_lock do
fuga_logic
end
end
end
- この場合
action_hoge
とaction_fuga
を同時に処理できなくなってしまい、スループット低下のリスクがあります
回避方法
lockするレコードを変えてスコープを小さくする
class HogeController
def action_hoge
hoge = Hoge.find(params[:id])
hoge.with_lock do
hoge_logic
end
end
end
with_lockの使い所
口座.残高のようなDB設計のとき、安全に残高を更新する
-
残高を減らす
のようなとき、with_lockの後で残高を確認する必要があることに注意してください - with_lockすると内部的にはreloadが行われ、最新のDBの状態に更新されます。そのため、with_lock前後で値が変わっている可能性があります
class 口座
# OK
def 残高を減らす(money)
with_lock do
raise 'error' if self.残高 < money
self.残高 -= money
end
end
# NG
def 残高を減らす(money)
raise 'error' if self.残高 < money # ここでOKでも...
with_lock do
# この時点で残高が足りていない可能性がある
self.残高 -= money
end
end
end
ActiveRecord.serializeを使っているとき、安全に内容を更新する
# NG
class Hoge < ApplicationRecord
serialize :preferences, Hash
def update_a
preferences[:a] = 1
save!
end
def update_b
preferences[:b] = 2
save!
end
end
Hoge.create!(preferences: {a: 0, b: 0})
# terminalを2つ開いて順に実行してみる
# terminal1
hoge.update_a
# => #<Hoge id: 1, preferences: {:a=>1, :b=>0}>
# terminal2
hoge.update_b
# => #<Hoge id: 1, preferences: {:a=>0, :b=>2}>
# terminal1の更新がなかったことになってしまう
# OK
class Hoge < ApplicationRecord
serialize :preferences, Hash
def update_a_with_lock
with_lock do
preferences[:a] = 1
save!
end
end
def update_b_with_lock
with_lock do
preferences[:b] = 2
save!
end
end
end
その他 個人的に使う別の手法
optimistic lock(lock version)を使う
Redisでロックする
- https://github.com/leandromoreira/redlock-rb
- Redisをすでに導入しているならおすすめの手法です
- ユーザーIDをキーの一部に組み込むことで、DBレコードがなくても排他制御できます
lock_manager.lock("lock_key/#{user.id}/some_logic", 2000) do |locked|
if locked
# critical code
else
# error handling
end
end