2013-10-03 14 views
5

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:

Pessimistic lock in T-SQL

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.

+0

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 –

+0

@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

+0

Ecco perché non l'ho postato come risposta. –

risposta

1

Verificare che il livello di isolamento del database sia stato impostato su READ COMMITTED.

SET TRANSACTION ISOLATION LEVEL READ COMMITTED 

Questo è il livello di isolamento predefinito. Assicura che tutte le transazioni devono essere confermate prima della lettura della riga, quindi non si verificano letture sporche.

Anche una nota importante, quando si aggiorna la tabella InvoiceNumbers, assicurarsi che sia in una transazione, si desidera applicare i principi ACID qui e tutto deve essere atomico (committente come un'unità intera o la transazione viene ripristinata).

+0

Grazie. Avrei dovuto ricordare che il codice dell'applicazione C# esegue il wrapping di questa operazione e di qualsiasi altra scrittura che si verifica in un DbTransaction. Quindi, se questo è il caso, questa procedura memorizzata dovrebbe essere libera da condizioni di gara? – JulianR

Problemi correlati