Open3

排他制御のメモ

naon708naon708

企業ごとのレコード数が10万件超え かつ そこそこの頻度で更新されるレコードを操作するときの排他制御のメモ

Rails コンソールでお試し

プロセスAでレコードAをロックする

# プロセスA
ActiveRecord::Base::transaction do
  article.lock!
  sleep 10
  article.update!(title: '今日の昼ごはん')
end

プロセスBでレコードAに update をかけようとする

# プロセスB
article.update!(title: '今日の晩ごはん')

プロセスAがロックを取得しているため、プロセスBはプロセスAのトランザクションがコミットされるまで待ってからレコードを更新する。

SQL確認

lock を取得するとSQLの最後に FOR UPDATE 句が追加された。

SELECT
  ...
FROM
  articles
WHERE
  ...
FOR UPDATE

整理

1つのレコードを複数のプロセスが同時に更新しようとしたとき、競合状態となりデータの整合性が合わなくなる(= レースコンディション)。
同時に操作できるプロセスを制限することを排他制御と呼ぶ。
その排他制御を実現するための行為や、排他制御されている状況のことをロックと呼ぶ。
「ロックを取る」「ロックをかける」と表現したりする。

用語メモ

  • 排他制御
  • ロック
  • 悲観ロック
  • 楽観ロック
  • トランザクション
  • レースコンディション(競合状態)
naon708naon708

〇〇ロックがたくさんあるが、概念として別のもの。

  • 排他ロック/共有ロック
  • 悲観ロック/楽観ロック

排他ロック/共有ロック

具体的なロックの方法。

  • 排他ロック(= 専有ロック)
    • 自分だけ read と write が可能。
    • 他人は read も write も受け付けない。
    • 「私以外誰も受け付けません。私がリソースを更新します。」
  • 共有ロック
    • 自分も他人も read のみ可能。
    • 自分も他人も write ができない。
    • 「みんな、読み取りだけして良いよ。私も読むだけ。」

悲観ロック/楽観ロック

ロックの思想や考え方のようなものらしい。
悲観ロックは「競合状態がそこそこありそうだから、ちゃんとロックしておこう。」
楽観ロックは「競合状態にはあまりならなそうだがら、もしぶつかったら中止してやり直そう。」

👇のように明示的に排他ロックの制御を行うのが悲観ロック。

ActiveRecord::Base.transaction do 
  articles = Article.lock.where(status: :publish)
  articles.find_each do |article|
    article.update!(title: 'hoge')
  end
end

一方楽観ロックはテーブルでバージョン管理し、UPDATE 時にリソース取得時のバージョンと比較して変更があれば ActiveRecord::StaleObjectError を返す。

👇のように例外処理するっぽい

begin
  ...
  article.save!
rescue ActiveRecord::StaleObjectError
  # 他プロセスで変更があって処理を中断したことをユーザーにしらせるなど
end

ちなみに lock_version カラムを追加すると Rails が自動で楽観ロックの仕組みを提供してくれるらしい。
https://railsguides.jp/active_record_basics.html#スキーマの規約