はじめに
データベース同時実行時の競合を検出したり、それに対処したりする方法については、数多くの記事が書かれています。
残念ながら、こうした記事と、そこに記されているソリューションのほとんどは、大きな欠陥を抱えています。いずれも、実際のデータとそのデータの使い方ではなく、技術的な問題とデータベースの実装に焦点が当てられているのです。本稿では、データベースの実装に焦点を当てることと、実環境のデータに焦点を当てることの違いを説明し、これらの同時実行時の問題を解決する方法の有効なアプローチをいくつか紹介します。
データベース同時実行時の競合とは何か
初めに、データベース同時実行時の競合とは何か、なぜそれを解決する必要があるかを簡単に説明しましょう。
データベースアプリケーションの大半はマルチユーザーアプリケーションです。つまり、任意の時点で、複数のユーザー/プロセスが同じデータベースとの間で、読み取り/書き込みを行うことが予想されます。複数のユーザー/プロセスが同じデータベース内のデータを更新しているとすれば、2人/2つの異なるユーザー/プロセスが、同時に同一のデータの更新を試みることは時間の問題です。通常の更新サイクルは次の一連のアクションから成り立ちます。
- メモリにデータを読み込む
- メモリ内のデータを更新する
- データをデータベースに書き込む
従って、2人のユーザーがメモリに同じデータを読み込むこともあるわけです。そのデータをユーザー2が更新して変更をデータベースに書き込む前に、ユーザー1が同じことをするかもしれません。ここで同時実行制御の競合が発生します。なぜなら、ユーザー2が持っているデータは、ユーザー1がデータベースにデータを書き込む前に読み取ったものだからです。ユーザー2がデータをデータベースに書き込めば、ユーザー1が加えた変更を上書きすることになり、結果的にユーザー1の変更が失われてしまいます。基本的には、最後に変更を保存した人の勝ちになります。先に保存した人の変更は、その後に保存された変更によって上書きされてしまいます。
この種のデータベース同時実行の問題は、ユーザー間でも、自動化されたプロセス間でも、またその両者の間でも発生する可能性があります。ユーザー間の方が、読み取り/更新/書き込みサイクルの時間が長いので同時実行の問題が発生する確率が高くなります。しかし、自動化されたプロセス間でも同じ問題が発生する可能性はあり、通常はこちらの方が解決が困難です。ユーザーによる更新の場合は、ユーザーに何をしたいか(他のユーザーが加えた変更を上書きしたいか)を質問してその答えを得ることができますが、プロセスの場合は、すべてのアクションを完全に自動化する必要があるからです。
現在のアプローチ
まず、データベースの同時実行問題の解決策として一般的に言われていることと行われていることを説明しましょう。通常、この問題の解決は、2つの基本的なアプローチに分けることができます。
- ペシミスティック(悲観的)な同時実行制御
- オプティミスティック(楽観的)な同時実行制御
問題を処理するために、この2つの異なる選択肢について簡単に説明します。ここでは、問題を明確にすることだけに焦点を当て、問題処理の適用範囲とその性質についての説明は一切省略します。
ペシミスティックな同時実行制御
ペシミスティックな同時実行制御では、アプリケーションがデータを変更する前にユーザー/プロセスにあるアクションを要求することで競合を防止します。この「アクション」は複数の要素から構成することができますが、通常は、データベース内のデータをロックし、それによって別のユーザーが同じロックを保持できないようにします。
長所と短所
長所:
- 実装が簡単 -- データベースサーバーはロックメカニズムをサポートし実施しているため、ペシミスティックな同時実行制御の実装は非常に簡単です。ユーザーは変更を加える前にロックをかける必要があるので、データベースサーバーは変更前に競合があることをユーザーに通知します。
- 非常に安全 -- データベースサーバーでのロックの実装は非常に信頼性が高いため、絶対にロックを無視してデータを変更できないことが保証されます。
短所:
- スケーラビリティに劣る -- データベース内のデータをロックするには、データベースとの接続を確立する必要があります。つまり、すべてのユーザーが少なくとも1つのデータベース接続を確立しなければならず、それだけ多くのリソースとライセンスが必要になります。また、使っているデータベースサーバーが古い場合は、ロックによって他のユーザーがデータの読み取りすらできなくなる可能性があります。
- デッドロックが起こりやすい -- 2人のユーザーがどちらもデータAとBを変更したいとします。ユーザー1は最初にAをロックし、ユーザー2は最初にBをロックします。2人とも、もう一方のデータをロックしたいのですが、既にロック済みであるため、ロックをかけることができません。2人ともデータが使用可能になるまで待つことにした場合、デッドロックが発生します。
- ロックが長時間続く可能性がある -- ユーザーがデータを変更し始めたら、そのデータはユーザーが保存するまでロックされたままとなります。しかし、ユーザーが他のことに気を取られたり、変更を保存せずに会議に行ったとしても、データはロックされたままです。最初の変更がコミットされるまで、他の誰も変更を加えることができません。
ペシミスティックなロックには、実際のデータベースロックではなくソフトロックを使うこともできます。この方法は、あるフィールドを更新することによって、ユーザーがデータを使用中でありデータが「ロック中」であることを示します。接続を確立する必要がないため、スケーラビリティの問題はなくなります。
しかし、このアプローチには欠点があります。データベース側でロックを実施するのではないため、別のコードによってロックが無視される可能性があります。また、ロックを手動で解除しなければならないので、解除しないとデータは永遠にロックされたままになります。
ペシミスティックなロックはそれなりに意味があるものの、欠点も多くあり、.NETアプリケーションではデータが分離されているため、あまりうまく活用できません。
オプティミスティックな同時実行制御
オプティミスティックな同時実行制御では、ユーザーがデータをロックすることで同時実行問題の発生を防ぐのではなく、データベースに書き込むときにその問題を検出して解決します。通常、開発者はさまざまなアプローチからオプティミスティックな同時実行制御を行っています。例えば次のようなアプローチがあります。
- チェックを行わず、最後のものを優先する
- ユーザーが変更したフィールドを比較する
- すべてのフィールドを比較する
- 行バージョンを比較する
1.チェックを行わず、最後のものを優先する
これは、サーバーが単に問題を無視するだけなので、本当のことを言えば同時実行制御のメカニズムではありません。最後にデータを更新するユーザーが、それまでのユーザーが加えた変更を上書きします。この場合のSQL UPDATE
ステートメントは、WHERE
句で主キーのみをフィルタとして使用します。この種の同時実行制御はシングルユーザーアプリケーションにしか適していません。
2.ユーザーが変更したフィールドを比較する
この場合は、更新処理の中で、ユーザーが変更したいデータとデータベース内のデータを比較し、ユーザーがデータを読み取ったときと同じデータであることが確認できた場合は変更をコミットします。最初に読み取ったデータとデータベース内のデータが異なる場合、サーバーはユーザーの変更をコミットせずに、ユーザーに警告を発行します。この場合のSQL UPDATE
ステートメントは、主キーと、古い値を持つ変更対象のフィールドと、WHERE
句を使用します。
3.すべてのフィールドを比較する
この種のオプティミスティックな同時実行制御のアプローチでは、更新をコミットする前に、変更されたフィールドだけでなく、すべてのフィールドをチェックします。これは手間のかけすぎのように思えますが、標準のADO.NET
クラスの性質からすると、変更されたフィールドだけをチェックするよりも効果的です。ADO.NET
データクラスの特徴は、ユーザーが変更したフィールドだけではなく、すべてのフィールドを使ってSQL UPDATE
ステートメントを実行することです。つまり、現在のユーザーが変更しようとする特定のフィールドについては他に変更したユーザーがいなくても、別の列のフィールドを他のユーザーが変更している可能性があるということです。この場合のSQL UPDATE
ステートメントは、テーブル内のすべてのフィールドを含むSQL WHERE
句を使用します。
4.行バージョンを比較する
このアプローチでは、データに行バージョンフィールドという追加フィールドを用意します。このフィールドは、タイムスタンプフィールドとも言い、サーバーはデータを更新するたびにこのフィールドを変更します。この方法だと、サーバーはSQL UPDATE
ステートメント内の主キーと行バージョンフィールドについてのみフィルタ処理を行えばよいので、すべてのフィールドを比較する場合よりも処理が単純になり、時間も短縮されます。この方法はすべてのフィールドを比較する場合と同じ動作に見えるかもしれませんが、結果的には異なるため、注意が必要です。
あるユーザーが現在の値ですべてのデータを更新するという更新処理を送信した場合も、データの行バージョンは更新されます。つまり、同じ行に何か変更を加えたい2番目のユーザーがいる場合、サーバーが行バージョンをチェックするアプローチでは競合が検出されますが、すべてのフィールドを比較するアプローチの場合は競合が検出されることはありません。
長所と短所
長所:
- スケーラビリティに優れている -- アプリケーションはデータベースと接続を確立しておく必要がないので、大人数のユーザーに対応することができます。
- 実装が簡単 -- 特に、行バージョンフィールドを使う場合は簡単です。
- デッドロックのおそれがほとんどない -- データベースサーバーはアプリケーションの動作を妨げるロックを持ち続けることがないため、デッドロックが発生するおそれもありません。唯一リスクがあるとすれば、アプリケーションロジックそのものの問題です。
短所:
- 安全性に劣る -- データベースは使用中のデータをロックすることができないため、不正なアプリケーションが行バージョンを無視してデータを構わずに更新してしまう可能性が残ります。
- 単一行指向である -- すべての行を単一の作業単位として扱います。これが特に当てはまるのが、行バージョンフィールドです。