一連の処理に排他ロックをかける
トランザクション・ロックについて
データの整合性を保つために、トランザクションはたびたび使われる。
銀行の入金処理がよく例に挙げられるが、A 口座から B 口座に対して 5000 円を振り込みむ際には、
-
対象となる口座残高を読み込む
-
A 口座残高を 5000 円減らす
-
B 口座残高を 5000 円増やす
これらの処理が一連している必要がある。トランザクションなしで 3 の処理中にエラーが発生した場合、
A 口座の 5000 円が消失してしまうことになる。
トランザクションを使うことで、失敗した場合にはトランザクション前の状態までロールバックする。
ただ、上記のトランザクション処理を行なっている途中で C 口座から B 口座への振り込みが行なわれた場合には処理が重複してしまうため、
2 つのトランザクション終了時点で B 口座の残高が合計 5000 円しか増えないことがある。
処理の重複によるをデータの矛盾を防ぐために、適切にロックを実装する必要がある。
Rails でのロックについて
ActiveRecord にはいくつかロックのメソッドが用意されている。
ActiveRecord::Locking::Pessimistic
lock
, lock!
はインスタンスに使うことでそのレコードをロックする。クエリに FOR UPDATE
が追加されるので、ロックがかかったレコードの読み込みは可能だが書き込みは待機となる。
# select * from accounts where name = 'shugo' limit 1 for update nowait
shugo = Account.lock(
- FOR UPDATE NOWAIT).find_by(name:
- shugo)
このように引数にクエリ句を渡せる。上記だと排他ロックとなり、読み込みで待機となる。
# File activerecord/lib/active_record/locking/pessimistic.rb, line 67
def lock!(lock = true)
if persisted?
if has_changes_to_save?
raise(<<-MSG.squish)
Locking a record with unpersisted changes is not supported. Use
`save` to persist the changes, or `reload` to discard them
explicitly.
MSG
end
reload(lock: lock)
end
self
end
lock!
はレコードが DB に保存されておらず、DB に保存されていない変更が該当するインスタンスにある場合には例外を発生させる。そうでない場合にレコードにロックをかける。
with_lock
はブロックを渡すことができ、トランザクションとロックを同時にかけることができる。
# File activerecord/lib/active_record/locking/pessimistic.rb, line 85
def with_lock(lock = true)
transaction do
lock!(lock)
yield
end
end
メソッドのなかではトランザクション内で lock!
を実行し、メソッドに渡したブロックを実行している。
MySQL5.7 の GET_LOCK, RELEASE_LOCK
MySQL では排他ロックの機能として GET_LOCK(str, timeout)
, RELEASE_LOCK(str)
がある。
前者は任意の文字列に対してロックをかけ、第 2 引数ではロックの取得までの待機時間を指定できる。第 2 引数を渡さない場合には待機せず 0 を返す。後者は任意の文字列に対するロックを解放する。
ActiveRecord には MySQL 用のクラスがあり、用意されているメソッドを使うことでもロックを実装できる。
rails/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb:L140-L146
def with_lock(lock_str)
if ActiveRecord::Base.connection.get_advisory_lock(lock_str)
begin
yield
rescue => e
raise e
end
else
raise DuplicateError
end
ensure
ActiveRecord::Base.connection.release_advisory_lock(lock_str)
end