Open3
排他制御のメモ
企業ごとのレコード数が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つのレコードを複数のプロセスが同時に更新しようとしたとき、競合状態となりデータの整合性が合わなくなる(= レースコンディション)。
同時に操作できるプロセスを制限することを排他制御と呼ぶ。
その排他制御を実現するための行為や、排他制御されている状況のことをロックと呼ぶ。
「ロックを取る」「ロックをかける」と表現したりする。
用語メモ
- 排他制御
- ロック
- 悲観ロック
- 楽観ロック
- トランザクション
- レースコンディション(競合状態)
〇〇ロックがたくさんあるが、概念として別のもの。
- 排他ロック/共有ロック
- 悲観ロック/楽観ロック
排他ロック/共有ロック
具体的なロックの方法。
- 排他ロック(= 専有ロック)
- 自分だけ 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 が自動で楽観ロックの仕組みを提供してくれるらしい。
デッドロック
WIP