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:
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.
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 –
@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
Dlatego nie napisałem tego jako odpowiedź. –