Webアプリケーションにおいて、複数のユーザーが同時に同じデータを読み書きすることはとても多いです。 しかし、適切な対策を行わないと、データの整合性が崩れる大きなトラブルに繋がります。
今回は、Railsにおける「悲観的ロック」と「楽観的ロック」の使い分けについて解説します
1:複数のユーザーが同時にアクセスすると何が起きるのか?
複数のユーザーが同じレコードを同時に更新しようとした場合、主に以下の2つの問題が発生します。
(1)更新内容の消失(ロストアップデート)
ユーザーAとユーザーBが同時にデータを読み込み、それぞれが更新して保存した場合、「先に保存したユーザーAのデータ」が「後から保存したユーザーBのデータ」によって上書きされ、消えてしまう現象です。
(2)ダーティリード(不整合なデータの読み取り)
あるユーザーがデータを更新している最中の「中途半端な状態」のデータを、別のユーザーが読み込んでしまう問題です。これにより、計算結果が狂うなどのバグが発生します。
これらの問題を解決するために必要なのが「ロック(排他制御)」という仕組みです。
2. ロック(排他制御)とは?
ロックとは、あるユーザーがデータを操作している間、他のユーザーによるそのデータへのアクセスや変更を制限する仕組みのことです。
3:悲観的ロック(Pessimistic Locking)
特徴
「他のユーザーも同時に更新してくるに違いない」と悲観的(慎重)に考え、データを読み込む時点でロックをかける方式です。
Railsでの実装
Railsでは lock メソッドや with_lock メソッドを使用します。データベースレベルで SELECT ... FOR UPDATE というSQLが発行され、トランザクションが終了するまで他の接続からの更新をブロックします。以下の例はemailがnilに更新することを保護します。
ActiveRecord::Base.transaction do user = User.lock.find_by(id: 1) # ロック中なので、他のユーザーはこの処理が終わるまで待たされる user.email = nil user.save! end # トランザクション終了とともにロックが解除される
4:楽観的ロック(Optimistic Locking)
リソース競合が少ない(更新頻度が低い)シーンで利用されます。楽観的ロックでは、データ衝突の可能性が低いという前提に立ち、複数ユーザーによる同時編集を許容します。
仕組みとしては、読み取り後に他プロセスによる更新があったかを検証し、競合があれば ActiveRecord::StaleObjectError を発生させて更新を無効化します。
Railsでの実装
テーブルに lock_version という整数型のカラムを追加するだけで自動的に有効になります。データを読み込む(この時の lock_version が 1 だとする)。保存する時、SQLのWHERE句に WHERE lock_version = 1 を含める。もし先に誰かが更新して lock_version が 2 になっていたら、更新に失敗し ActiveRecord::StaleObjectError が発生します。
5. まとめ
悲観ロックはエラーの発生確率が低いというメリットがあります。なぜなら、一度ロックを取得すると、他のプロセスはブロックされるため、同時更新による競合を防ぐことができるからです。ただし、その分処理速度に影響が出やすく、システム全体のオーバーヘッドも大きくなります。そのため、高い並行性が求められる環境では不利になることもあります。
一方で、楽観ロックはリソース競合がそれほど多くない場面に適しています。ロックによる待ち時間が少ないため、システムのオーバーヘッドを抑えることができ、結果として処理速度も向上しやすいのが特徴です。