Tech Hotoke Blog

IT観音とは私のことです。

【DB × Rails】トランザクション、排他制御について 時々 Rails

What is this?

一般的なトランザクション排他制御に関するまとめとそれをRailsで実現するためのメモ書き

Assumption

  • RDBSにおける話
  • Railsを使う前提

トランザクションについて

> ITの分野では、取引記録などの意味の他に、ソフトウェアの処理方式の一つで、互いに関連・依存する複数の処理をまとめ、一体不可分の処理単位として扱うことを指す場合が多い。
  • 要するに、相互に関係する処理をこれ以上分割できない一つのまとまりとして捉えたもの

    例)銀行口座の送金処理(出金と入金処理が不可分一体となっている)

  • トランザクションにおいてはACID特性が保たれる必要がある

ACID特性について

排他制御について

  • 代表的な例
    • ロック: 共有資源(DBやファイル)にロックをかけて同時にアクセスさせない方式
    • ミューテックス:「未使用」と「使用中」で判断し、共有資源を同時にアクセスさせない方式
    • セマフォ:同時にアクセスできる数を管理し、その人数までしか共有資源に対して同時にアクセスさせない方式。

今回取り扱うのはロックに限ります。

  • 代表的なロック
    • 楽観的ロック
    • 悲観的ロック
    • 共有ロック
    • 排他ロック

今回は、特によく使われる(印象の)悲観ロックと楽観ロックについてまとめます。(Active Recordに備わっているロック機構もこの二種類なので)

悲観ロックと楽観ロックについて

  • 悲観ロック

    • 前提:他者が同時更新は頻繁に起きる
    • 方法:SELECT 〜 FOR UPDATEなど
    • デメリット:ロックの解放漏れがあるとツラい(多分最近のFWはこの辺りもよしなにやってくれているので、意識しなくてもいいことが多いような気がする)
    • 備考:一般的に採用される。ECサイトのセールような決まった時間に同時多発的にアクセスが集中するような場合が典型的な気がする。
  • 楽観ロック

    • 前提:他者との同時更新は滅多に起きない
    • 方法:資源そのものにロックはかけずに、更新対象のデータがデータ取得時と同じ状態かを確認してから更新を行う
    • デメリット:更新されている場合に処理が全てロールバックされるため、処理待ちや処理のやり直しの負荷が高まる。
    • 備考:採用されるシーンはあまり多くない。同時アクセスが起こりづらいがデータの整合性は保たれねばならない場合に使う。例えば経理の管理画面など?

Railsにおける排他制御

楽観ロック

  • テーブルにlock_versionという名前のinteger型カラムを作成する
  • Active Recordは、レコードが更新されるたびにlock_versionカラムの値を1ずつ増やす
  • 更新リクエストが発生したときのlock_versionの値がデータベース上のlock_versionカラムの値よりも小さい場合、更新リクエストは失敗し、ActiveRecord::StaleObjectErrorエラーが発生する(例外処理が必要
  • ActiveRecord::Base.lock_optimistically = falseを設定するとこの動作をオフにできる
  • ActiveRecord::Baseには、lock_versionカラム名を上書きするためのlocking_column属性が用意されている

悲観ロック

  • リレーションの構築時にlockを使うと、選択した行に対する排他的ロックを取得できる
  • lockを用いているリレーションは、デッドロック(互いの処理がロックされて処理が進まなくなった状態)条件を回避するために通常トランザクションの内側にラップされる
例

Book.transaction do
  book = Book.lock.first
  book.title = 'Algorithms, second edition'
  book.save!
end
Mysql

SQL (0.2ms)   BEGIN
Book Load (0.3ms)   SELECT * FROM `books` LIMIT 1 FOR UPDATE
Book Update (0.4ms)   UPDATE `books` SET `updated_at` = '2009-02-07 18:05:56', `title` = 'Algorithms, second edition' WHERE `id` = 1
SQL (0.8ms)   COMMIT

SELECT 〜 FOR UPDATEが内部的に走ってるんですね

Railsガイドには他のロックを実装する方法も乗っているので折に触れてそちらも確認して行こうと思います。

参考