🔒

【Rails】悲観ロックと楽観ロックの違いを理解する

に公開

はじめに

ロックとは?

ロックとは、複数のユーザーや処理が同じデータを同時に変更しようとしたときに、データの不整合を防ぐ仕組みです。
たとえば、ある在庫数をAさんとBさんが同時に減らそうとした場合、ロックがないと最終的な在庫が意図しない数になってしまう可能性があります。

トランザクションとは?

トランザクションとは、複数の処理を1つのまとまりとして扱い、すべてが成功するか、すべて失敗してロールバックするという仕組みです。
これにより、データの整合性や一貫性を保証することができます。


ロックの種類

悲観ロック(Pessimistic Lock)

悲観ロックは、「どうせ競合するはずだ」という前提で、データを読み込んだ時点でロックをかける手法です。他のトランザクションはそのデータにアクセスできなくなります。

Railsでの書き方

Book.transaction do
  book = Book.lock.find(1)
  book.title = "New Title"
  book.save!
end

悲観ロックの動作検証(rails consoleでの手順)

前提準備

セッションA(ターミナル1)

Book.transaction do
  book = Book.lock.find(1)
  puts "A: ロック取得済み"
  sleep 30
  book.title = "散歩"
  book.save!
  puts "A: 更新完了"
end

セッションB(ターミナル2)

Book.transaction do
  puts "B: ロック取得前"
  book = Book.lock.find(1)
  puts "B: ロック取得完了"
  book.title = "昼寝"
  book.save!
  puts "B: 更新完了"
end

実行結果の確認

  • セッションBは、セッションAがsave!してトランザクションを終了するまで**find(1) で待たされる**
  • ロックが解放された後、セッションBが処理を再開する

楽観ロック(Optimistic Lock)

楽観ロックは、「たぶん競合しないだろう」という前提で、ロックをかけずに処理を行い、更新時に競合していたら例外を投げてリトライする手法です。

Railsでの使い方

lock_version という integer 型カラムを用います。

# migration
add_column :books, :lock_version, :integer, default: 0, null: false

# controllerなど
book = Book.find(1)
book.title = "Updated title"
book.save! # => lock_version が変わっていたら ActiveRecord::StaleObjectError

競合の発生例

# ユーザーA
book_a = Book.find(1)

# ユーザーB
book_b = Book.find(1)
book_b.title = "From B"
book_b.save! # lock_version 0 → 1 に

# ユーザーAが保存しようとすると...
book_a.title = "From A"
book_a.save!
# => ActiveRecord::StaleObjectError が発生

リトライ処理の例

MAX_RETRIES = 3
retries = 0

begin
  book.save!
rescue ActiveRecord::StaleObjectError
  retries += 1
  if retries <= MAX_RETRIES
    book.reload
    retry
  else
    raise "競合が解決できませんでした"
  end
end

特徴

  • データベースにロック負荷をかけない
  • 競合が少ない場面で有効
  • リトライ処理が必要

悲観ロックと楽観ロックの比較

項目 悲観ロック 楽観ロック
前提 競合するはず 競合しないはず
ロックのタイミング 読み取り時点 更新時点(バージョンチェック)
実装方法 lock, with_lock, lock! lock_version カラムを使用
デッドロックの可能性 あり なし
向いている場面 同時更新が頻繁に起きる 稀にしか競合しない

まとめ

  • 悲観ロックは「安全第一」で先にロックをかけて他のトランザクションをブロック
  • 楽観ロックは「高速優先」でロックせず、競合があったらエラーで検知
  • 運用状況やデータの性質に応じて使い分けるのが重要です!
ラブグラフのエンジニアブログ

Discussion