Mam tabelę, która jest używana przez starszą aplikację jako substytut IDENTITY
pól w różnych innych tabelach.
Każdy wiersz w tabeli przechowuje ostatnio używany identyfikator LastID
dla pola o nazwie wIDName
.
Czasami przechowywany proc dostaje impasu - wydaje mi się, że zbudowałem odpowiedni moduł obsługi błędów; jednak jestem zainteresowany, aby zobaczyć, czy ta metodologia działa tak, jak myślę, czy też szczekam tutaj niewłaściwe drzewo.
Jestem całkiem pewien, że powinien istnieć sposób na uzyskanie dostępu do tego stołu bez żadnych zakleszczeń.
Sama baza danych jest skonfigurowana za pomocą READ_COMMITTED_SNAPSHOT = 1
.
Po pierwsze, oto tabela:
CREATE TABLE [dbo].[tblIDs](
[IDListID] [int] NOT NULL
CONSTRAINT PK_tblIDs
PRIMARY KEY CLUSTERED
IDENTITY(1,1) ,
[IDName] [nvarchar](255) NULL,
[LastID] [int] NULL,
);
I indeks nieklastrowany w IDName
polu:
CREATE NONCLUSTERED INDEX [IX_tblIDs_IDName]
ON [dbo].[tblIDs]
(
[IDName] ASC
)
WITH (
PAD_INDEX = OFF
, STATISTICS_NORECOMPUTE = OFF
, SORT_IN_TEMPDB = OFF
, DROP_EXISTING = OFF
, ONLINE = OFF
, ALLOW_ROW_LOCKS = ON
, ALLOW_PAGE_LOCKS = ON
, FILLFACTOR = 80
);
GO
Niektóre przykładowe dane:
INSERT INTO tblIDs (IDName, LastID)
VALUES ('SomeTestID', 1);
INSERT INTO tblIDs (IDName, LastID)
VALUES ('SomeOtherTestID', 1);
GO
Procedura przechowywana używana do aktualizacji wartości przechowywanych w tabeli i zwracania następnego identyfikatora:
CREATE PROCEDURE [dbo].[GetNextID](
@IDName nvarchar(255)
)
AS
BEGIN
/*
Description: Increments and returns the LastID value from tblIDs
for a given IDName
Author: Max Vernon
Date: 2012-07-19
*/
DECLARE @Retry int;
DECLARE @EN int, @ES int, @ET int;
SET @Retry = 5;
DECLARE @NewID int;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SET NOCOUNT ON;
WHILE @Retry > 0
BEGIN
BEGIN TRY
BEGIN TRANSACTION;
SET @NewID = COALESCE((SELECT LastID
FROM tblIDs
WHERE IDName = @IDName),0)+1;
IF (SELECT COUNT(IDName)
FROM tblIDs
WHERE IDName = @IDName) = 0
INSERT INTO tblIDs (IDName, LastID)
VALUES (@IDName, @NewID)
ELSE
UPDATE tblIDs
SET LastID = @NewID
WHERE IDName = @IDName;
COMMIT TRANSACTION;
SET @Retry = -2; /* no need to retry since the operation completed */
END TRY
BEGIN CATCH
IF (ERROR_NUMBER() = 1205) /* DEADLOCK */
SET @Retry = @Retry - 1;
ELSE
BEGIN
SET @Retry = -1;
SET @EN = ERROR_NUMBER();
SET @ES = ERROR_SEVERITY();
SET @ET = ERROR_STATE()
RAISERROR (@EN,@ES,@ET);
END
ROLLBACK TRANSACTION;
END CATCH
END
IF @Retry = 0 /* must have deadlock'd 5 times. */
BEGIN
SET @EN = 1205;
SET @ES = 13;
SET @ET = 1
RAISERROR (@EN,@ES,@ET);
END
ELSE
SELECT @NewID AS NewID;
END
GO
Przykładowe wykonania zapisanego proc:
EXEC GetNextID 'SomeTestID';
NewID
2
EXEC GetNextID 'SomeTestID';
NewID
3
EXEC GetNextID 'SomeOtherTestID';
NewID
2
EDYTOWAĆ:
Dodałem nowy indeks, ponieważ istniejący indeks IX_tblIDs_Name nie jest używany przez SP; Zakładam, że procesor zapytań korzysta z indeksu klastrowego, ponieważ potrzebuje wartości przechowywanej w LastID. W każdym razie ten indeks JEST używany przez rzeczywisty plan wykonania:
CREATE NONCLUSTERED INDEX IX_tblIDs_IDName_LastID
ON dbo.tblIDs
(
IDName ASC
)
INCLUDE
(
LastID
)
WITH (FILLFACTOR = 100
, ONLINE=ON
, ALLOW_ROW_LOCKS = ON
, ALLOW_PAGE_LOCKS = ON);
EDYCJA 2:
Skorzystałem z porady udzielonej przez @AaronBertrand i nieco ją zmodyfikowałem. Ogólną ideą tutaj jest udoskonalenie instrukcji w celu wyeliminowania niepotrzebnego blokowania i ogólnie w celu zwiększenia wydajności SP.
Poniższy kod zastępuje powyższy kod od BEGIN TRANSACTION
do END TRANSACTION
:
BEGIN TRANSACTION;
SET @NewID = COALESCE((SELECT LastID
FROM dbo.tblIDs
WHERE IDName = @IDName), 0) + 1;
IF @NewID = 1
INSERT INTO tblIDs (IDName, LastID)
VALUES (@IDName, @NewID);
ELSE
UPDATE dbo.tblIDs
SET LastID = @NewID
WHERE IDName = @IDName;
COMMIT TRANSACTION;
Ponieważ nasz kod nigdy nie dodaje rekordu do tej tabeli z wartością 0 LastID
, możemy założyć, że jeśli @NewID ma wartość 1, to intencją jest dodanie nowego identyfikatora do listy, w przeciwnym razie aktualizujemy istniejący wiersz na liście.
źródło
SERIALIZABLE
tutaj.Odpowiedzi:
Po pierwsze, unikałbym podróży w obie strony do bazy danych dla każdej wartości. Na przykład, jeśli aplikacja wie, że potrzebuje 20 nowych identyfikatorów, nie rób 20 podróży w obie strony. Wykonaj tylko jedno wywołanie procedury składowanej i zwiększ licznik o 20. Również lepiej podzielić tabelę na wiele.
Możliwe jest całkowite uniknięcie impasu. W moim systemie nie ma żadnych blokad. Można to osiągnąć na kilka sposobów. Pokażę, jak użyłbym sp_getapplock do wyeliminowania zakleszczeń. Nie mam pojęcia, czy to zadziała, ponieważ SQL Server jest zamkniętym źródłem, więc nie widzę kodu źródłowego i jako taki nie wiem, czy przetestowałem wszystkie możliwe przypadki.
Poniżej opisano, co działa dla mnie. YMMV.
Po pierwsze, zacznijmy od scenariusza, w którym zawsze dostajemy znaczną liczbę impasów. Po drugie, użyjemy sp_getapplock, aby je wyeliminować. Najważniejszą kwestią jest przetestowanie rozwiązania w warunkach skrajnych. Twoje rozwiązanie może być inne, ale musisz narazić je na wysoką współbieżność, co pokażę później.
Wymagania wstępne
Ustawmy tabelę z niektórymi danymi testowymi:
Następujące dwie procedury prawdopodobnie przyjmą impas:
Odtwarzanie zakleszczeń
Następujące pętle powinny odtwarzać ponad 20 zakleszczeń za każdym razem, gdy je uruchamiasz. Jeśli otrzymasz mniej niż 20, zwiększ liczbę iteracji.
Na jednej karcie uruchom to;
Na innej karcie uruchom ten skrypt.
Upewnij się, że zaczniesz oba w ciągu kilku sekund.
Używanie sp_getapplock do eliminowania zakleszczeń
Zmień obie procedury, uruchom ponownie pętlę i przekonaj się, że nie masz już impasu:
Korzystanie ze stołu z jednym rzędem w celu wyeliminowania zakleszczeń
Zamiast wywoływać sp_getapplock, możemy zmodyfikować następującą tabelę:
Po utworzeniu i wypełnieniu tej tabeli możemy zastąpić następujący wiersz
z tym, w obu procedurach:
Możesz ponownie wykonać test warunków skrajnych i przekonać się, że nie mamy impasu.
Wniosek
Jak widzieliśmy, sp_getapplock może służyć do szeregowania dostępu do innych zasobów. Jako taki może być stosowany do eliminowania zakleszczeń.
Oczywiście może to znacznie spowolnić modyfikacje. Aby temu zaradzić, musimy wybrać odpowiednią szczegółowość blokady wyłącznej i, o ile to możliwe, pracować z zestawami zamiast poszczególnych wierszy.
Przed użyciem tego podejścia musisz przetestować je samodzielnie. Po pierwsze, musisz upewnić się, że otrzymujesz co najmniej kilkadziesiąt impasów przy swoim oryginalnym podejściu. Po drugie, nie powinieneś mieć żadnych zakleszczeń po ponownym uruchomieniu tego samego skryptu repro przy użyciu zmodyfikowanej procedury składowanej.
Ogólnie rzecz biorąc, nie sądzę, że istnieje dobry sposób na ustalenie, czy Twój T-SQL jest bezpieczny przed zakleszczeniem, po prostu patrząc na niego lub patrząc na plan wykonania. IMO jedynym sposobem ustalenia, czy twój kod jest podatny na zakleszczenia, jest wystawienie go na wysoką współbieżność.
Powodzenia w eliminowaniu zakleszczeń! W naszym systemie nie ma żadnych blokad, co doskonale wpływa na równowagę między pracą a życiem prywatnym.
źródło
UPDATE dbo.DeadlockTestMutex SET Toggle = 1 - Toggle WHERE ID = 1;
zapobiega zakleszczeniom?Użycie
XLOCK
podpowiedzi zarówno do twojegoSELECT
podejścia, jak i poniższych,UPDATE
powinno być odporne na tego rodzaju impas:Powróci z kilkoma innymi wariantami (jeśli nie zostanie pobity!).
źródło
XLOCK
zapobiegnie aktualizacji istniejącego licznika z wielu połączeń, czy nie potrzebujesz,TABLOCKX
aby zapobiec dodawaniu tego samego nowego licznika przez wiele połączeń?Mike Defehr pokazał mi elegancki sposób na osiągnięcie tego w bardzo lekki sposób:
(Dla kompletności, oto tabela związana z przechowywanym proc)
Oto plan wykonania najnowszej wersji:
Oto plan wykonania oryginalnej wersji (podatny na zakleszczenie):
Oczywiście nowa wersja wygrywa!
Dla porównania, wersja pośrednia z
(XLOCK)
itp. Tworzy następujący plan:Powiedziałbym, że to wygrana! Dzięki za pomoc wszystkich!
źródło
SERIALIZABLE
nie istnieje, aby zapobiec fantomom. Istnieje, aby zapewnić szeregowalną semantykę izolacji , tj. Taki sam trwały wpływ na bazę danych, jak gdyby dane transakcje zostały wykonane szeregowo w jakiejś nieokreślonej kolejności.Nie kradnie grzmotu Marka Storey'ego-Smitha, ale jest na czymś, co ma wyżej postawiony post (który, nawiasem mówiąc, otrzymał najwięcej pozytywnych opinii). Porada, której udzieliłem Maxowi, była skoncentrowana wokół konstrukcji „UPDATE set @variable = column = column + value”, którą uważam za naprawdę fajną, ale myślę, że może być nieudokumentowana (musi być obsługiwana, chociaż dlatego, że jest specjalnie dla TCP testy porównawcze).
Oto odmiana odpowiedzi Marka - ponieważ zwracasz nową wartość identyfikatora jako zestaw rekordów, możesz całkowicie zrezygnować ze zmiennej skalarnej, żadna wyraźna transakcja również nie powinna być konieczna, i zgodziłbym się, że bałagan z poziomami izolacji jest niepotrzebny także. Rezultat jest bardzo czysty i bardzo zręczny ...
źródło
Naprawiłem podobny impas w systemie w zeszłym roku, zmieniając to:
Do tego:
Ogólnie rzecz biorąc, wybranie
COUNT
tylko określenia obecności lub nieobecności jest dość marnotrawstwem. W tym przypadku, ponieważ jest to 0 lub 1, to nie jest tak, że to dużo pracy, ale (a) ten nawyk może wykrwawić się w innych przypadkach, w których będzie znacznie droższy (w takich przypadkach użyjIF NOT EXISTS
zamiastIF COUNT() = 0
), i (b) dodatkowe skanowanie jest całkowicie niepotrzebne. TheUPDATE
wykonuje zasadniczo taki sam czek.Poza tym wygląda mi to na poważny zapach kodu:
O co tu chodzi? Dlaczego po prostu nie użyć kolumny tożsamości lub wyprowadzić tej sekwencji przy użyciu
ROW_NUMBER()
w czasie zapytania?źródło
IDENTITY
. Ta tabela obsługuje niektóre starsze kody napisane w MS Access, które byłyby dość zaangażowane w modernizację.SET @NewID=
Linia po prostu zwiększa wartość zapisaną w tabeli dla danego ID (ale już wiesz, że). Czy możesz rozwinąć sposób, w jaki mógłbym użyćROW_NUMBER()
?LastID
tak naprawdę oznacza Twój model. Jaki jest jego cel? Nazwa nie jest do końca zrozumiała. Jak korzysta z niego Access?GetNextID('WhatevertheIDFieldIsCalled')
aby uzyskać kolejny identyfikator do użycia, a następnie wstawia go do nowego wiersza wraz z potrzebnymi danymi.