12周年記念パーティ開催! 2024/5/10(金) 19:00

Webアプリ頻出実装を考える 第1回 データは誰のもの?

Webアプリで出くわす「共同編集」の場面。複数人が同一のデータを操作するので、当然、データの衝突が起こりえます。どのようにデータをハンドリングするのか、考えてみましょう。

発行

著者 德田 和規 テクニカルディレクター
Webアプリ頻出実装を考える シリーズの記事一覧

はじめに

更新されうる同一のデータに対してアクセス可能なユーザーが1人しかいない場合、基本的にそのユーザーのことだけを考えればいいわけで、そこにデータの衝突が起きるということはあまり考える必要はないでしょう。また起こったとしても、ユーザーは1人なわけですから、エラーとして処理すればいいだけです。

ところが、データに対して複数のユーザーがいる場合はどうでしょうか。この場合、考えなければいけないことは、1人のときは起きなかったデータの衝突です。同じデータに対して違うことを書き込もうとしているのですから、当然です。

例えば、ユーザーAとBが少しずれたタイミングで同じデータに対して違う変更を行って保存をしようとした場合は、どのように処理されるのでしょうか?

Webアプリでもこのような「共同編集」が可能な場面はよくあります。

このように複数のユーザーによるデータ保存が行われた場合のデータのあり方を見ていきましょう。

データの更新パターン

さて、すでに例にあげたように、複数のユーザーで同一のデータを操作(主に保存)する可能性があるデータ更新は、大きく分けると2通りのパターンがあります。

  • データは先勝ち:データは先に更新した人のもので、先に変更した人の保存状態を保証する
  • データは後勝ち:データは後からいくらでも上書きできるが、先に変更した人の保存状態は保証されない

2つのパターンの違いは、先に変更した人の保存状態を保証するかしないかという違いですが、これはかなり重要なことです。ちなみに、実装するときに何も考えなくてよいのは「後勝ち」です。単に保存リクエストが投げられてきたら、保存すればよいだけですから……。

データの後勝ち

まずはデータの後勝ちについて、もう少し詳しく見てみましょう。話をシンプルにするために、ユーザーはAとBの2人で考えます。

ユーザーAとBが同時にデータを受け取り、まずAが操作し、データを更新したとします。この時点でサーバーの最新データはAが操作したものになっています。

しかしその後に、Bがまったく違う操作をして保存しました。どのようになるかは明確で、後勝ちが採用されていますから、サーバーのAが操作したデータは、Bのデータで上書きされ、なかったことになってしまいます。

このように後勝ちというのは、複数ユーザーで同一のデータを扱う場合には明らかに向いていません。自分が操作したデータが消えてしまうのですから、ユーザーAはこの仕様をバグとして捉えてしまうのではないでしょうか。

データの先勝ち

次は先勝ちですが、さきほどと同じように操作を具体的に考えてみます。

ユーザーAとBが同時にデータを受け取り、まずAが操作し、データを更新したとします。この時点でサーバーの最新データはAが操作したものになっています。

しかしその後にBがまったく違う操作をして保存しました。

ここまでは先ほどと同じです。ですがBの更新作業への対応が異なります。

Bの保存は、データの先勝ちの原則にしたがって、成功せずにエラーになります。データはAが更新した時点でロックされてしまうのです。

これが先勝ちです。

これが先勝ちです、と言うのは簡単なのですが、実は仕様を考えるのも、実装するのもなかなか手間がかかるのです。

例えば、先に更新した人のデータで、更新より前に取得したデータではロックされてしまって更新できなくすると言いましたが、それでは、以降、ほかのユーザーはどのようにして更新すればよいのでしょうか。

ずっとロックされたままでは、誰も更新できなくなってしまいます。

先勝ちの仕様

では、先勝ちの仕様について考えてみましょう。

ちなみに、先勝ちは基本的にサーバーとフロントエンドで連携して行いますので、サーバーサイドによる処理の場合は、そのつど、その旨を表記しています。

データの先勝ちを実現するためには、ユーザーがアクセスまたは該当するデータを取得したときに、その時点での何らかの情報(例えばトークンやタイムスタンプなど)を持っている必要があります。

具体的には、次のような方法が考えられます。

  • ページにセッションを管理したトークンが埋め込まれていて、ユーザーはアクション時に毎回このトークンを渡して、サーバーに処理させる
  • 取得するデータにタイムスタンプなどのデータが最新とわかる情報を持たせて、ユーザーはそれを受け取り更新時に一緒に渡してサーバーに処理させる

先勝ちによるデータ保存とその後の振る舞い

ここで挙げたトークン、タイムスタンプどちらを利用するにしても、保存からその後の流れは同じです。

  1. データ更新時に、サーバーにトークン(タイムスタンプ)を渡す
  2. 渡された情報を元にデータをロックする
  3. ロックされたデータを更新するためには、渡されたデータがロックされた時間よりも後に発行されたトークン(タイムスタンプ)を持つことが必要
  4. ほかのユーザーはトークン(タイムスタンプ)を取得し直さないとデータを更新できない

ロックされたデータに保存しようとした場合の振る舞い

先勝ちにより、ロックされたデータはそれ以降、また最新のトークン(タイムスタンプ)を取得しない限り更新できなくなりましたが、保存しようとした場合はどのように振る舞うべきでしょうか。

例えば、このような振る舞いが考えられます。

  1. ロックされていて更新できない旨を伝えるエラーを表示する
  2. エラーにロック解除ボタンを表示しておく
  3. 解除ボタンにより、最新のトークン(タイムスタンプ)を再取得する
  4. 更新ができるようになる

ほかにも、次のような、あまり手間のかからない方法も考えられます。

  • ページをリロードさせてデータを初期化し直させる
  • しばらくすると自動的にロックが解除される

ユーザーに優しいのは、やはり少し手間ですが、その場でトークンやタイムスタンプを取得し直してもらう方法でしょう。

先勝ちの実装仕様

ここでは先勝ち仕様の例を見てみましょう。まず、この例では、次のような仕様で実装しています。

  1. GET時にタイムスタンプを受け取る
  2. PUT時に1.で受け取ったタイムスタンプを渡す
  3. サーバーサイド:データをロックして、レスポンスで最新のスタンプを返す
  4. ほかの人はタイムスタンプを取得し直さないとデータを更新できない

1. GET時にタイムスタンプを受け取る

例えばupdated_atなどで一番最後に更新された時間を返してもらいます。

updated_at

{
  created_at: "2014-07-17T06:45:26Z",
  data: { ... },
  id: 103,
  name: "トップページ",
  path: "/index.html",
  published: "true",
  updated_at: "2014-09-19T06:15:44Z'
}

updated_atを受け取ります。

2. PUT時に1.で受け取ったタイムスタンプを渡す

PUTするときは、**1.**で受け取ったupdated_atをサーバーに渡します。最新のupdated_atを持っているということは、このユーザーは現時点で最新のデータに対して変更を加えようとしているので、このPUTは行ってもよい、という判定になります。

request_headers

{
  created_at: "2014-07-17T06:45:26Z",
  data: {
    ... ,
    updated_at: "2014-09-19T06:15:44Z"
  },
  id: 103,
  name: "トップページ",
  path: "/index.html",
  published: "false",
  updated_at: "2014-09-19T06:15:44Z'
}

dataオブジェクトの中に受け取ったupdated_atをコピーして、サーバーに渡します。

3. サーバーサイド:データをロックして、レスポンスで最新のスタンプを返す

データがPUTされた時点で、サーバー側では、**2.**で渡されたデータを更新し、ロックします。この時点で、すべてのユーザーはデータに対しての更新権を失います。以降このデータに対するPUTは、更新されたupdated_atを持っていることが条件になります。PUTをしたユーザーに対しては引き続き更新権を持たせるため、PUTのレスポンスで、最新のupdated_atを返します。ほかのユーザーもGETし直せば更新が可能です。

response

{
  published: "false",
  updated_at: "2014-09-24T00:30:14Z"
}

4. ほかの人はタイムスタンプを取得し直さないとデータを更新できない

流れとしては、**3.**までの通りですが、最新のupdated_atを持っていないユーザーによる更新はエラーとして扱います。

表示の画像例では、やり直すボタンをクリックすることで、最新のデータを再取得します。その際にエラーになったユーザーの変更は消えてしまいますが、保存の粒度を細かく設定しておくことで、データの喪失被害を最低限に抑えることができるでしょう。

まとめ

複数人のユーザーが同時にデータを変更するような可能性がある場合、このように先勝ちというルールを設けておけば、ユーザーの混乱はかなり抑えることができるでしょう。

ここでは挙げていませんが、そもそも複数人によるデータ更新をさせない、というのもひとつの手です。その場合は、どのタイミングでデータのロックを解除するのか、が論点になりそうです。

複数人で編集したいという仕様がどうしても譲れないのであれば、ここであげたように、実装は少し大変だとしても、なるべくユーザーには負担をかけないように設計しておきたいですね。