より長続きするブログ

続けていきたい気持ち。

複数人による同時編集でロストアップデートするのを防ぐ

もう散々語り尽くされているのかもしれないが、Web画面で、同じリソースの複数人による同時編集でロストアップデートするのを防ぐ方法が自分の中でちゃんと確立できていなかったので考えた。

流れはこんな感じ。

  1. 編集対象をブラウザで開いたとき、リソースの最終更新日時など、最新リソースであることを確認できるデータを取得する
  2. 編集してアップデートするとき、取得していた最終更新日時のようなデータも一緒に送信する
  3. 更新するまえに最終更新日時のようなデータをselect for updateで取得し、一致しているかチェックする
  4. 一致していたら更新し、違っていたらエラーにする

以上でOKなはず。

 

ポイントとして、更新前のデータ取得でselect for updateを使う。

複数のトランザクションが走っていたら、2番目以降の更新はここで止まり、その後読み込むと既に別のトランザクションがコミットされているので履歴IDは一致せずエラーにできる。

 

一致を確認するデータとして「最終更新日時のようなデータ」と書いたが、リソースの変更履歴テーブルのようなものを用意して、そのバージョンが変わっていないか見るというのがより良いと思う。特に一つのモデルが複数のテーブルから成っているときは最終更新日時はいくつかある…みたいな状態になり、モデルごとの更新履歴テーブルは使いやすい。

 

で、運用上はリクエストに履歴IDを付けるのは必須じゃなくて、付いていたら一致を確認して、無かったらそのままアップデートみたいな緩い感じでもいいかなと思っている。

 

ちょっと混乱していたのだが、この挙動はトランザクション分離レベルでは解決しずらい。

SERIALIZABLEなら、解決するけどそれは普通やらないし。他の分離レベルで他のトランザクションからの影響をなくしてもあんまり意味がない。他のトランザクションに対して待ちを発生させる必要がある。

 

懸念点として、select for updateで最新のデータを取得する…というのはこういうコードになるとおもうのだが、

SELECT * from change_log a
where not EXISTS (
  select * from change_log b
  where a.created_at < b.created_at

) for update;

 

 行ロックではなくテーブルロックになってしまうようである。行ロックにする方法があればいいけれど、「最新」というのはテーブル全体をロックしておかないと確定できないような気がするので仕方ないのかもしれない。

まあ、同時更新絶対防止したいようなリソースは更新頻度低いだろうし、ロック掛けても大丈夫なように作れることが多いかとは思うけど。