重複を防ぐための IsolationLevel.RepeatableRead
-
02-07-2019 - |
質問
私は、PayPal インスタント支払い通知を受信したときに商品 (配送保険証券など) を作成するアプリケーションに取り組んでいます。残念ながら、PayPal は重複した通知を送信することがあります。さらに、PayPal から更新を取得するときに同時に Web サービスの更新を実行する別のサードパーティもあります。
以下に、関連するデータベース テーブルの基本的な図を示します。
// table "package"
// columns packageID, policyID, other data...
//
// table "insurancepolicy"
// columns policyID, coverageAmount, other data...
これが私がやりたいことの基本的な図です:
using (SqlConnection conn = new SqlConnection(...))
{
sqlTransaction sqlTrans = conn.BeginTransaction(IsolationLevel.RepeatableRead);
// Calls a stored procedure that checks if the foreign key in the transaction table has a value.
if (PackageDB.HasInsurancePolicy(packageID, conn))
{
sqlTrans.Commit();
return false;
}
// Insert row in foreign table.
int policyID = InsurancePolicyDB.Insert(coverageAmount, conn);
if (policyID <= 0)
{
sqlTrans.Rollback();
return false;
}
// Assign foreign key to parent table. If this fails, roll back everything.
bool assigned = PackageDB.AssignPolicyID(packageID, policyID, conn);
if (!assigned)
{
sqlTrans.Rollback();
return false;
}
}
これを同時に実行している 2 つ (またはそれ以上) のスレッド (またはプロセスまたはアプリケーション) がある場合、ポリシーが作成されてポリシー ID が割り当てられるまで、ポリシー ID がない間、最初のスレッドが「パッケージ」行をロックするようにします。パッケージテーブルに。その後、policyID がパッケージ テーブルに割り当てられた後にロックが解放されます。これと同じコードを呼び出している他のスレッドが、パッケージ行を読み取るときに一時停止して、最初にポリシー ID がないことを確認することを願っています。最初のトランザクションのロックが解放されると、2 番目のトランザクションはポリシー ID が存在することを確認し、ポリシー テーブルに行を挿入せずに戻ることを期待しています。
注記:CRUD データベースの設計により、各ストアド プロシージャには読み取り (選択)、作成 (挿入)、または更新のいずれかが含まれます。
これは、RepeatableRead トランザクション分離の正しい使用法ですか?
ありがとう。
解決
そうすればもっと安全できれいになるのに insert into Policy
重複を挿入しようとすると、テーブルの一意性制約にヒットするだけです。分離レベルを上げると同時実行性が低下し、デッドロックなどの他の厄介な問題が発生する可能性があります。
もう 1 つの方法は、常にポリシー行を挿入し、パッケージがすでにポリシーにアタッチされている場合はロールバックすることです。
begin tran (read committed)
/* tentatively insert new Policy */
insert Policy
/* attach Package to Policy if it's still free */
update Package
set Package.policy_id = @policy_id
where Package.package_id = @package_id and Package.policy_id is null
if @@rowcount > 0
commit
else
rollback
これは、競合がまれである場合に最も効果的です。あなたのケースはそのような場合です。
他のヒント
実際にはシリアライズ可能な分離レベルが必要だと思います。問題は、2 つのスレッドが HasInsurancePolicyCheck を通過できることです (ただし、InsurancePolicyDB.Insert が何をするのか、なぜ 0 を返すのかはわかりません)。
これには他にも多くのオプションがあります。1 つは、メッセージ キューを使用して、これらのリクエストを自分で逐次処理する方法です。もう1つは使用することです sp_getapplock そして、そのパッケージに固有のキーをロックします。こうすることで、必要以上の行やテーブルをロックすることがなくなります。
私は aaronjensen の回答の「メッセージキュー」の考えに同意します。複数の同時スレッドが同じデータ行を同時に更新しようとすることが懸念される場合は、代わりに、スレッドにデータを作業キューに挿入させ、単一のスレッドによって順次処理されるようにする必要があります。これにより、ターゲット テーブルが「N」ではなく 1 つのスレッドのみによって更新され、ワーク キューの操作がメッセージング スレッドによる挿入とデータ処理スレッドによる読み取り/更新に制限されるため、データベースの競合が大幅に軽減されます。