optimistic lockとpessimistic lock

railsでselectの結果によって更新するデータを変更する条件分岐を書いていて、selectしてデータを更新するまでの間に他のプロセスによってデータが更新される可能性があることに気づきます。

たとえば以下のようなプログラムだと、


1 user = User.find(1)
2
3 if user.status == 1
4 user.update_attributes(:status => 2)
5 elsif user.status == 3
6 user.update_attributes(:status => 4)
7 end
8
9 unless user.valid?
10 ...

1行目でfindしてから4行目もしくは6行目に至るまでの間に、対象のデータが更新されていることに気づかずにデータを更新してしまいます。

データベース初心者なり対策を調べてみました。

1.update_allを使う


1 user = User.find(1)
2
3 if user.status == 1
4 result = User.update_all("status = 2", "id = 1 AND status = 1")
5 elsif user.status == 3
6 result = User.update_all("status = 4", "id = 1 AND status = 1")
7 end
8
9 if result == 1
10 ...

条件を絞り込んでデータを更新します。インスタンスを作っているのに、クラスメソッドでデータを更新してかっこ悪いような気がします。

2.pessimistic lockを使う
データベースが実装しているロックを使って、selectしてからupdateするまでの間に他のプロセスにデータを更新させない方法です。悲観的なロックというらしいです。
ActiveRecordに標準で実装されています。


1 User.transaction do
2 user = User.find(1, :lock => true)
3
4 if user.status == 1
5 user.update_attributes(:status => 2)
6 elsif user.status == 3
7 user.update_attributes(:status => 4)
8 end
9
10 end
11 ...

ロック中に他のプロセスがデータを更新しようとした場合、ロックが解除されるまでそのまま待たされることになるようです。気がかりなのはロックしたプロセスがロックしたまま死んでしまったときです。ロックを解除しない限りデータの更新ができなくなるのが怖いです。

3.optimistic lockを使う
アプリケーション(rails)が用意しているロック機構を使うロックです。selectからupdaetの間のデータ更新は早いもの勝ちですが、updateのときにデータが更新されていたらエラー(StaleObjectError例外)になります。これを使うと、少なくともロックしたプロセスに不具合が起きて長時間データが更新できなくなるというトラブルはなさそうです。カラムにlock_versionを追加するだけで機能するようです。
使用する際の注意としては以下の点があると思います。

  • findで:selectオプションを使っている場合は、idとlock_versionを含めないと機能しない。
  • オブジェクトのアトリビュート(テーブルのカラム)がひとつでも変更されればlock_versionがインクリメントされるので、複数のアトリビューションをもつ場合には意図しないときにStaleObjectError例外が発生してしまうかもしれない。