2013-10-03 12 views
5

Mam prawne wymaganie, że w kolekcji faktur naszej aplikacji (przy użyciu programu SQL Server) nie może być luki w naszej numeracji. Tak więc, jeśli byłyby to numery faktur, nie byłoby to dozwolone: ​​[1, 2, 3, 4, 8, 10], ponieważ nie jest sekwencyjne. W tym celu mamy kolumnę InvoiceNumber na naszej tabeli Invoices. Oprócz tego mamy tabelę InvoiceNumbers, która przechowuje aktualny numer faktury na organizację (ponieważ każda organizacja musi mieć własną sekwencję). Procedura składowana jest następnie odpowiedzialna za wypełnianie atomowo; albo zwiększa bieżący licznik o 1 w tabeli InvoiceNumbers i wypełnia tę nową wartość w tabeli Invoices, albo wycofuje transakcję w przypadku błędu. To działa dobrze.Tworzenie numerów sekwencyjnych faktur w SQL Server bez warunku wyścigowego

Dodano nowe wymaganie: niektóre zamówienia muszą mieć tę samą fakturę, a tym samym ten sam numer faktury, podczas gdy wcześniej każde zamówienie było fakturowane osobno. W tym celu tworzymy fakturę na początku dnia i łączymy ją z bieżącym FinancialPeriod (dzień roboczy, zasadniczo), który będzie fakturą używaną przy każdym zamówieniu. Możliwe jednak, że organizacja nie tworzy żadnych zleceń typu, które wymagają wspólnego fakturowania, a więc nie ma faktury w ciągu dnia, który "marnuje" pierwotnie utworzoną fakturę (ponieważ następnego dnia zostanie utworzona nowa) i tworzy luka.

Teraz najłatwiej było mi leniwie wypełnić InvoiceNumber na fakturze udostępnionej, która zostanie utworzona na początku dnia. Jeśli zamówienie zostanie utworzone tego dnia, a InvoiceNumber nadal będzie to NULL, utwórz numer. Dzięki temu fakturowanie nigdy nie będzie nieużywane (nie ma znaczenia, że ​​rekord Invoice jest nieużywany, nie ma on prawdziwego znaczenia).

W tym celu utworzyłem poniższą procedurę przechowywaną, która dla istniejącego Invoice wypełnia InvoiceNumber, ale tylko wtedy, gdy nadal jest NULL. Nie mam pewności, jak SQL Server blokuje się i czy istnieje potencjał warunku wyścigowego, w którym dwie transakcje bazy danych decydują, że InvoiceNumber nadal będzie miał wartość NULL i zwiększy licznik i zmarnuje jeden numer, tworząc lukę.

Zasadniczo, to rozwlekłe pytanie sprowadza się do: czy dwie jednoczesne transakcje z bazami danych decydują się na wprowadzenie do bloku if(@currentNumber is null) dla tego samego?

Część zamek widać stałam się stąd, ale nie jestem pewien, że to odnosi się do mojego przypadku:

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

Jak wspomniano w mój komentarz, te i inne operacje zapisu są częścią transakcji bazy danych zainicjowanej z logiki aplikacji C#. Po prostu zwykły BeginTransaction na SqlConnection z opcjami domyślnymi, który jest oczywiście wycofywany w przypadku jakichkolwiek wyjątków.

+0

Zobacz to pytanie, a to odpowiedź na wypróbowane i prawdziwe rozwiązanie Twojego problemu. http://dba.stackexchange.com/questions/36603/handling-concrentrent-access-to-a-key-table-without-deadlocks-in-sql-server –

+0

@MaxVernon - Popraw mnie, jeśli się mylę, ale wydaje się, że ma on większy cel w robieniu tego, co robię, ale unikaniu i wychodzeniu z impasu, co nie jest dla mnie priorytetem. Tak czy inaczej, trudno jest wydobyć to, czego potrzebuję, z tej odpowiedzi, ponieważ jest to bardziej złożone niż to, co do tej pory miałem. – JulianR

+0

Dlatego nie napisałem tego jako odpowiedź. –

Odpowiedz

1

Upewnij się, że poziom izolacji bazy danych został ustawiony na READ COMMITTED.

SET TRANSACTION ISOLATION LEVEL READ COMMITTED 

To jest domyślny poziom izolacji. Gwarantuje, że wszystkie transakcje muszą zostać zatwierdzone przed odczytaniem wiersza, dlatego nie pojawiają się brudne odczyty.

Ważna uwaga przy aktualizowaniu tabeli , upewnij się, że jest w transakcji, chcesz, aby zasady ACID były tutaj stosowane i aby wszystko było atomowe (zatwierdzenie jako całość lub transakcja wycofana).

+0

Dziękuję. Powinienem wspomnieć, że kod aplikacji C# opakowuje tę operację i wszelkie inne zapisy, które występują w DbTransaction. Więc jeśli tak jest, ta procedura przechowywana powinna być wolna od wyścigów? – JulianR

Powiązane problemy