013 同一レコードを複数ユーザが同時に変更してしまうのを防ぐには?


こんにちは、id:EC-OneのAkiです。

梅雨も明けて30度を超える暑い日が続きますが、みなさんいかがお過ごしですか?
アイスの食べ過ぎは夏バテの元ですよ!
今回は、Webアプリケーションのちょっと生っぽい実装テクニックのお話です。

同一レコードを複数ユーザが同時に変更してしまう?

データベース内の特定のレコードを変更するWebアプリケーションを考えてみます。たとえば、「取引先マスタの変更」を行うアプリケーション等です。
この場合、このアプリケーションは以下のような流れになるでしょう。

  1. ユーザがデータ編集画面に入るボタンをクリック。
  2. サーバが現在のデータをデータベースから取得&返却し、それをユーザのブラウザが表示する。
  3. ユーザがデータを編集し、編集結果反映ボタンをクリック。
    • このとき、変更した項目も変更していない項目も一緒にサーバに送られる。
  4. サーバが編集結果をデータベースに反映する。
    • このとき、変更した項目も変更していない項目も一緒にデータベースに反映(上書き)される。

上記の流れには、実は「編集開始から編集結果反映までの間に他のユーザもそのデータを変更できる」という問題があります。
たとえば、こんなことが起こりえてしまうのです。

  • Aさんがデータ編集画面に入る。
    • Bさんもデータ編集画面に入る。
  • Aさんが項目Aを編集して編集結果反映を行った。
    • Bさんが項目Bを編集して編集結果反映を行った。

この時、最後のBさんの操作によって、直前にAさんが編集した項目Aは元の値に戻ってしまうのです!

クラサバなら出来たこと、Webだと出来ないこと

昔のクライアント/サーバ型のアプリケーションであれば、クライアントが編集を行っている間、当該レコードをロックしておいて他のユーザが編集を開始する事を防ぐ、という手法もありました。クライアントが今もそこにいて編集中である、という事はTCP接続が保持されていることで保証できていたからです。

しかし、Webアプリケーションは、1リクエスト処理毎に一旦サーバとクライアント(ブラウザ)の会話が一旦切れるのが普通です。(TCP接続が複数リクエストをまたがって保持されていても、HTTPプロトコルとしてはやはり1問1答です)
そのため、データベースのトランザクションも1リクエスト処理毎に一旦終了するのが普通なので、「編集開始」リクエストと「編集結果反映」リクエストの間をまたがってレコードにロックをかけたままにするのも簡単には出来ません。わざわざ「ロック中であることを示すカラム」を作ってそれを参照/更新する、等の作りこみが必要になってしまいます。

しかも、Webアプリケーションには、「クライアント(ブラウザ)が今でもそこにいるのかわからない」「編集を開始した後、ブラウザを終了してしまったかもしれない」という事情があります。「ロックを開始したまま、帰ってくるかわからないクライアントを待ち続ける」事は現実的でないのです。

更新カウンタを使った先勝ち排他

こういった場合に使える方法があります。それは、

「先に更新した人を優先する。後から更新した人には『他の人が先に更新してしまいました』メッセージを表示する」

というものです。
この場合、後から更新した人は再度編集画面に入りなおし、最新のデータを元に編集作業を再開してもらう事になります。

では、その実現方法を以下に示します。

  1. データベースの当該テーブルに「更新カウンタ」という数値型のカラムを作成する。
    このカラムは、当該レコードが更新されるたびに値を+1する。
  2. 編集開始時、データベースから取得した「更新カウンタ」の値を、編集画面のHTMLのhiddenに保持しておく。
  3. 編集結果反映時、update SQLで以下を行う。
    • whereに「現在の更新カウンタの値が編集開始時の値であること」という条件を追加する。
      (編集開始時の値=HTMLのhiddenに保持していた値)
    • 更新カウンタの値を+1する。
  4. update SQLの実行結果をチェックし、「更新レコード数が1である事」を確認する。
    • もしも先に更新した人がいれば、where条件がマッチしないので、更新レコード数は0(ゼロ)になるはず。

上記のupdate文は以下のようになります。

update TABLE1
  set ..., UPDATE_COUNTER=mod(?+1, 100)
    where ... and UPDATE_COUNTER=?

「UPDATE_COUNTER」が更新カウンタのカラム名です。2箇所にある「?」には、HTMLのhiddenに保持していた値をバインド(はめ込み)します。

また、mod()関数を使っているので、UPDATE_COUNTERは0〜99を循環するのみで、100以上になる事はありません。

泥臭くても実用度は高い

ずいぶんと泥臭い方法だな、と思われた方もいらっしゃるかもしれません。
しかし、この方法は実現が簡単であるため、同じレコードを複数ユーザが同時に編集する頻度がさほど高くない場合には、意外に良く使われています。現場の知恵とでも言いましょうか・・・


ナレッジセンターでは、「ミドルウェアやライブラリの設定だけでは解決できそうにない問題、ベテランエンジニアならどうするの?」などのご質問も受け付けています。どうぞお気軽にご相談下さい!







JavaRuby及び周辺のOSSを用いた開発に関して、企業があらゆる悩みごとを相談できるのが、ナレッジセンターの「レスキューサービス」です。
どんな相談でも親身に受け付けますので、レスキューサービスってなに?もっと知りたい!と思った方はお気軽に問い合わせ下さい。
問い合わせ画像リンク