Ho un requisito legale che nella raccolta di fatture della nostra applicazione (utilizzando SQL Server) non ci possa essere una lacuna nella nostra numerazione. Quindi, se si tratta di numeri di fattura, questo non sarebbe consentito: [1, 2, 3, 4, 8, 10]
perché non è sequenziale. A tal fine, abbiamo una colonna InvoiceNumber
sulla nostra tabella Invoices
. In aggiunta a ciò, abbiamo una tabella InvoiceNumbers
che contiene il numero di fattura corrente per organizzazione (perché ogni organizzazione deve avere una propria sequenza). Una stored procedure è quindi responsabile della compilazione dello InvoiceNumber
su Invoices
atomicamente; incrementa il contatore corrente di 1 nella tabella InvoiceNumbers
e inserisce tale nuovo valore nella tabella Invoices
oppure esegue il rollback della transazione in caso di errore. Funziona beneCreazione di numeri di fattura sequenziali in SQL Server senza condizioni di gara
Ora è stato aggiunto un nuovo requisito: alcuni ordini devono condividere la stessa fattura e quindi lo stesso numero di fattura, mentre in precedenza ogni ordine veniva fatturato separatamente. A tal fine, creiamo una fattura all'inizio della giornata e la associamo all'attuale FinancialPeriod
(il giorno lavorativo, in sostanza) che sarà la fattura utilizzata per ogni ordine. Tuttavia, è possibile che un'organizzazione non crei ordini del tipo che richiedono la fatturazione condivisa e quindi non ha nulla da fatturare durante un giorno che "spreca" la fattura creata inizialmente (perché il giorno successivo ne viene creata una nuova) e crea una gap.
Ora, la soluzione più semplice per me è quella di riempire pigramente il InvoiceNumber
sulla fattura condivisa che viene creata all'inizio della giornata. Se un ordine viene creato quel giorno e è ancora NULL
, quindi creare il numero. Ciò garantirebbe che InvoiceNumber non vada mai inutilizzato (non importa se un record Invoice
non viene utilizzato, non ha alcun significato reale).
A tal fine, ho creato la procedura memorizzata in basso, che per un Invoice
esistente, riempie il InvoiceNumber
ma solo se è ancora NULL
. Sono solo incerto su come SQL Server blocca e se c'è un potenziale per una condizione di competizione in cui due transazioni di database decidono che InvoiceNumber
è ancora NULL
e entrambi incrementeranno il contatore e sprecheranno un numero, creando un gap.
In sostanza, questa domanda prolissa si riduce a: possono due transazioni di database simultanee decidere di immettere il blocco if(@currentNumber is null)
per lo stesso @invoiceID
qui?
La parte di bloccaggio che si vede che ho ottenuto da qui, ma non sono sicuro che si applica al mio caso:
CREATE PROCEDURE [dbo].[CreateInvoiceNumber]
@invoiceID int,
@appID int
AS
BEGIN
SET NOCOUNT ON;
if not exists (select 1 from InvoiceNumbers where ApplicationID = @appID) insert into InvoiceNumbers values (@appID, 1)
declare @currentNumber int = null;
select @currentNumber = convert(int, i.InvoiceNumber)
from Invoices i
with (HOLDLOCK, ROWLOCK)
where i.ID = @invoiceID
if(@currentNumber is null)
begin
update InvoiceNumbers set @currentNumber = Value = Value + 1
where ApplicationID = @appID
update Invoices set InvoiceNumber = @currentNumber where ID = @invoiceID
end
select convert(nvarchar, @currentNumber)
END
EDIT
Come accennato in il mio commento, queste e altre operazioni di scrittura fanno parte di una transazione di database avviata dalla logica dell'applicazione C#. Solo un normale BeginTransaction
su un SqlConnection
con opzioni predefinite, che è ovviamente ripristinato in caso di eventuali eccezioni.
Vedi questa domanda, ed è la risposta per una soluzione collaudata per il tuo problema. http://dba.stackexchange.com/questions/36603/handling-concurrent-access-to-a-key-table-without-deadlocks-in-sql-server –
@MaxVernon - Correggimi se sbaglio, ma sembra essere più mirato a fare quello che sto facendo, ma a evitare e riprendersi da deadlock, che non è una priorità per me. In ogni caso, è piuttosto difficile distillare ciò di cui ho bisogno da quella risposta, dal momento che è piuttosto più complesso di quello che ho ottenuto finora. – JulianR
Ecco perché non l'ho postato come risposta. –