LoginSignup
3
3

More than 1 year has passed since last update.

ActiveRecordのwith_lockをいつ使うか

Posted at
  • 気軽に使うのはおすすめできません

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_hogeaction_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
3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3