IsolationLevel.RepeatableLeggi per evitare duplicati
-
02-07-2019 - |
Domanda
Sto lavorando a un'applicazione che dovrebbe creare prodotti (come le polizze assicurative di spedizione) quando si ricevono le notifiche di pagamento istantaneo PayPal. Purtroppo, a volte PayPal invia notifiche duplicate. Inoltre, esiste un'altra terza parte che esegue aggiornamenti del servizio Web contemporaneamente quando ricevono anche aggiornamenti da PayPal.
Ecco un diagramma di base delle tabelle del database coinvolte.
// table "package"
// columns packageID, policyID, other data...
//
// table "insurancepolicy"
// columns policyID, coverageAmount, other data...
Ecco un diagramma di base di ciò che voglio fare:
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;
}
}
Se ci sono due (o più) thread (o processi o applicazioni) che lo fanno contemporaneamente, voglio che il primo thread blocchi il "pacchetto" riga mentre non ha policyID, fino a quando la policy non viene creata e policyID viene assegnato alla tabella del pacchetto. Quindi il blocco verrebbe rilasciato dopo che policyID è stato assegnato alla tabella dei pacchetti. Spero che l'altro thread che chiama questo stesso codice si interromperà quando legge la riga del pacchetto per assicurarsi che non abbia prima un ID politica. Quando viene rilasciato il blocco della prima transazione, spero che la seconda transazione vedrà il policyID presente e quindi ritorni senza inserire alcuna riga nella tabella delle politiche.
Nota: a causa della progettazione del database CRUD, ciascuna delle procedure memorizzate ha comportato la lettura (selezione), la creazione (inserimento) o l'aggiornamento.
È questo l'uso corretto dell'isolamento delle transazioni RepeatableRead?
Grazie.
Soluzione
Sarebbe più sicuro e più pulito se inserisca in Policy
colpisse solo un vincolo della tabella di unicità nel tentativo di inserire duplicati. Aumentare il livello di isolamento può ridurre la concorrenza e portare ad altre brutte questioni come i deadlock.
Un altro modo è inserire sempre la riga della politica, quindi ripristinarla se il pacchetto è già stato allegato a una politica:
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
Funziona meglio quando i conflitti sono rari, il che sembra essere il tuo caso.
Altri suggerimenti
Credo che tu stia realmente volendo il livello di isolamento serializzabile. Il problema è che due thread possono superare HasInsurancePolicyCheck (anche se non ho idea di cosa farebbe InsurancePolicyDB.Insert o perché restituirebbe 0)
Hai anche molte altre opzioni per questo. Uno sta usando una coda di messaggi ed elabora queste richieste in serie da soli. Un altro è usare sp_getapplock e bloccare alcune chiavi esclusive di quel pacchetto . In questo modo non blocchi più righe o tabelle del necessario.
Accetto la " coda messaggi " idea nella risposta di aaronjensen. Se sei preoccupato per più thread simultanei che tentano di aggiornare contemporaneamente la stessa riga di dati, dovresti invece fare in modo che i thread inseriscano i loro dati in una coda di lavoro, che viene quindi elaborata in sequenza da un singolo thread. Ciò riduce significativamente la contesa sul database, poiché la tabella di destinazione viene aggiornata da un solo thread anziché "N", e le operazioni della coda di lavoro sono limitate agli inserimenti dai thread di messaggistica e alla lettura / aggiornamento dal thread di elaborazione dei dati.