SQL Server w logistyce

READPAST w T-SQL: druga sesja bierze kolejne zadanie zamiast czekać na blokadę

W kolejce zadań w T-SQL wskazówka READPAST sprawia, że druga sesja bierze kolejne wolne zadanie zamiast czekać na blokadę. Kolejność według rangi dotyczy wtedy tylko zadań, które nie są w tej chwili zablokowane przez inną sesję.

sql.server.net.pl/sql-server/
Abstrakcyjny wirujący pierścień z pomarańczowymi panelami na tle ciemnego miasta
Abstrakcyjny wirujący pierścień z pomarańczowymi panelami na tle ciemnego miasta
W skrócie
  • Przejęcie: jedna instrukcja UPDATE z OUTPUT przejmuje zadanie i zwraca je od razu.
  • READPAST: druga sesja pomija zadanie zablokowane przez pierwszą i bierze kolejne wolne.
  • Kolejność: ranga porządkuje tylko zadania, których w tej chwili nie blokuje inna sesja.
  • Test: pomiar dwiema sesjami na poziomach zgodności 130 i 160.

W kolejce zadań w T-SQL wskazówka READPAST sprawia, że druga sesja bierze kolejne wolne zadanie zamiast czekać na blokadę. Programista albo administrator bazy buduje kolejkę zadań dla kilku terminali albo procesów: zadanie ma trafić tylko do jednego z nich, a druga sesja nie ma czekać na blokadę. Gdy pierwsza sesja trzyma zadanie w otwartej transakcji, druga sesja bez READPAST czeka na zwolnienie blokady, a z READPAST bierze kolejne wolne zadanie. Kolejność według rangi dotyczy wtedy tylko zadań, które nie są w tej chwili zablokowane przez inną sesję.

Druga sesja z READPAST dostaje kolejne wolne zadanie po 0 do 1 ms, a bez READPAST czeka około 2,5 s na zwolnienie blokady. Wariant z jedną instrukcją UPDATE i OUTPUT nie dał duplikatów w 500 zadaniach, a wariant z osobnym odczytem i zapisem dał ich około 490. READPAST nie gwarantuje kolejności według rangi.

W opisywanym wdrożeniu terminal pobiera następne zlecenie jednym zapytaniem, które zwraca najwyżej jedno zlecenie. Zlecenie po wejściu w zbiórkę zostaje przy swoim operatorze i nie wraca do kolejki innego. Pokazane tu warianty są wzorcami do przeniesienia do innej bazy i nie opisują kodu tego wdrożenia. Działanie terminala opisuje artykuł o kolejce zleceń na terminalu.

Odczyt następnego zadania i osobny zapis pozwalają dwóm sesjom wziąć to samo zadanie

W wariancie dwuetapowym odczyt zadania i zapis przejęcia są osobnymi instrukcjami, a między nimi powstaje odstęp, w którym druga sesja może odczytać to samo zadanie. W teście ten odstęp odtworzono przerwą 20 ms, która jest umowna i symuluje czas między odczytem a zapisem. Dwa procesy wykonały wtedy około 990 przejęć 500 zadań, czyli około 490 duplikatów, a liczba duplikatów zmieniała się między biegami. Ten wyścig opisuje osobny artykuł o znaczniku wysłano w jednym UPDATE. Aktualizacja warunkowa po odczycie, czyli UPDATE z warunkiem na status nowe i ponowieniem przy zerze zmienionych wierszy, jest drugą strategią i nie daje duplikatów: w teście dwa procesy przejęły 500 różnych zadań, z około 250 ponowień na proces.

Założenia przykładu. Pierwszy z trzech bloków kodu tworzy tabelę zadań z kolumnami ranga, status i aktywowano, indeks filtrowany pod kolejność oraz tabelę dziennika przejęć. Stoi w nim też wariant procedury bez READPAST, który w teście służy do pomiaru czekania. Tabela zadań startuje pusta, więc przed wywołaniem procedury trzeba dopisać własne wiersze ze statusem nowe. Drugi blok to procedura z READPAST, a trzeci to fragment pętli wariantu dwuetapowego, w którym zapis $(proces) jest nazwą procesu podstawianą przez narzędzie sqlcmd.

Instrukcja CREATE OR ALTER wymaga wersji silnika z poziomem zgodności 130 i z dodatkiem Service Pack 1 albo nowszej. Test wykonano na jednej instancji, na poziomach zgodności 130 i 160; starszych wersji silnika nie testowano. Poniżej trzy bloki kodu w kolejności uruchamiania. Pierwszy blok zawiera separator GO, który jest separatorem narzędzia, a nie instrukcją T-SQL.

-- tabela zadań, indeks pod kolejność, dziennik przejęć oraz wariant testowy bez READPAST.
SET NOCOUNT ON;
IF OBJECT_ID(N'dbo.przejecie_przyklad') IS NOT NULL DROP TABLE dbo.przejecie_przyklad;
IF OBJECT_ID(N'dbo.zadanie_przyklad') IS NOT NULL DROP TABLE dbo.zadanie_przyklad;
GO
CREATE TABLE dbo.zadanie_przyklad (
    id         int IDENTITY(1,1) NOT NULL PRIMARY KEY,
    nazwa      nvarchar(50)  NOT NULL,
    ranga      tinyint       NOT NULL,                      -- 3 wysoki, 2 normalny, 1 niski
    aktywne    bit           NOT NULL DEFAULT 1,
    status     varchar(10)   NOT NULL DEFAULT 'nowe',
    wlasciciel varchar(30)   NULL,
    aktywowano datetime2(3)  NOT NULL DEFAULT SYSDATETIME(),
    przejeto   datetime2(3)  NULL
);
CREATE INDEX ix_zadanie_kolejka ON dbo.zadanie_przyklad (ranga DESC, aktywowano, id)
    WHERE aktywne = 1 AND status = 'nowe';
CREATE TABLE dbo.przejecie_przyklad (
    id         int IDENTITY(1,1) PRIMARY KEY,
    proces     varchar(10) NOT NULL,
    zadanie_id int         NOT NULL,
    kiedy      datetime2(3) NOT NULL DEFAULT SYSDATETIME()
);
GO
-- wariant testowy: ta sama instrukcja, ale bez READPAST (druga sesja czeka na blokadę)
CREATE OR ALTER PROCEDURE dbo.nastepne_zadanie_bez_readpast_przyklad @operator varchar(30)
AS
BEGIN
    SET NOCOUNT ON;
    WITH nastepne AS (
        SELECT TOP (1) id, nazwa, ranga, status, wlasciciel, przejeto
        FROM dbo.zadanie_przyklad WITH (UPDLOCK, ROWLOCK)
        WHERE aktywne = 1 AND status = 'nowe'
        ORDER BY ranga DESC, aktywowano, id
    )
    UPDATE nastepne
    SET status = 'w_toku', wlasciciel = @operator, przejeto = SYSDATETIME()
    OUTPUT inserted.id, inserted.nazwa, inserted.ranga, inserted.wlasciciel;
END
GO

Struktura przykładu i wariant testowy bez READPAST.

CREATE OR ALTER PROCEDURE dbo.nastepne_zadanie_przyklad @operator varchar(30)
AS
BEGIN
    SET NOCOUNT ON;
    WITH nastepne AS (
        SELECT TOP (1) id, nazwa, ranga, status, wlasciciel, przejeto
        FROM dbo.zadanie_przyklad WITH (UPDLOCK, READPAST, ROWLOCK)
        WHERE aktywne = 1 AND status = 'nowe'
        ORDER BY ranga DESC, aktywowano, id
    )
    UPDATE nastepne
    SET status = 'w_toku', wlasciciel = @operator, przejeto = SYSDATETIME()
    OUTPUT inserted.id, inserted.nazwa, inserted.ranga, inserted.wlasciciel;
END

Procedura przejęcia następnego zadania z READPAST.

Wariant dwuetapowy, który pokazuje duplikaty, to fragment pętli procesu testowego.

SELECT TOP (1) @id = id FROM dbo.zadanie_przyklad
WHERE aktywne = 1 AND status = 'nowe' ORDER BY ranga DESC, aktywowano, id;
IF @id IS NOT NULL
BEGIN
    WAITFOR DELAY '00:00:00.020';
    UPDATE dbo.zadanie_przyklad
    SET status = 'w_toku', wlasciciel = '$(proces)', przejeto = SYSDATETIME()
    WHERE id = @id;
END

Fragment pętli procesu testowego: odczyt i zapis w dwóch instrukcjach.

Przejęcie zadania w jednej instrukcji zwraca je od razu i nie wymaga drugiego odczytu.

Z READPAST druga sesja bierze kolejne wolne zadanie zamiast czekać na blokadę

Procedura z READPAST składa się z jednej instrukcji UPDATE na wyrażeniu tabelowym: wyrażenie wybiera jedno zadanie aktywne ze statusem nowe, a UPDATE oznacza je jako przejęte przez operatora i zwraca przejęty wiersz klauzulą OUTPUT. Wyrażenie czyta zadania ze wskazówkami UPDLOCK oraz ROWLOCK obok READPAST i układa je według rangi malejąco, potem czasu aktywacji i identyfikatora. READPAST każe pominąć wiersze zablokowane przez inne sesje zamiast na nie czekać. W teście dwóch procesów i 500 zadań ten wariant przejął 500 różnych zadań i nie dał duplikatów. Podział zadań między procesy był zbliżony do połowy, od 239 do 261 zadań na proces, ale zależy od chwili startu i nie jest równy; asercją testu jest brak duplikatów, nie podział.

Czekanie zmierzono osobnym testem. Pierwsza sesja trzymała zadanie 2 w otwartej transakcji przez 4 s, a druga startowała po 1,5 s. Druga sesja z READPAST dostała zadanie 5 po 0 ms na poziomie zgodności 130 i po 1 ms na poziomie 160. Ta sama sesja bez READPAST czekała do zwolnienia blokady, czyli około 2,5 s (2495 i 2498 ms, a w powtórzonym teście od 2492 do 2510 ms). Czas oczekiwania jest więc ograniczony długością przytrzymania blokady i nie jest stałą wariantu: około 2,5 s to reszta czasu, przez który pierwsza sesja trzymała zadanie.

Wariant bez UPDLOCK, w którym zostają READPAST i ROWLOCK, przy 600 zadaniach i czterech procesach też nie dał duplikatów ani błędów, a druga sesja pomijała zablokowane zadanie bez czekania. Wyniki są zgodne na poziomach zgodności 130 i 160; różni się tylko podział zadań między procesy i milisekundy. Poniższa tabela zbiera pomiary, w tym odczyty logiczne omówione w ostatniej sekcji.

Wyniki pomiarów kolejki zadań
PomiarPoziom 130Poziom 160
Duplikaty, wariant z jedną instrukcją (500 zadań)00
Duplikaty, wariant dwuetapowy (500 zadań)około 490około 490
Czekanie drugiej sesji z READPAST (ms)01
Czekanie drugiej sesji bez READPAST (ms; blokada przytrzymana 4 s, druga sesja startuje po 1,5 s)24952498
Odczyty logiczne z indeksem filtrowanym99
Odczyty logiczne bez indeksurzędu 150-190rzędu 150-190

Pomiary na poziomach zgodności 130 i 160.

Makieta wyniku pomiaru kolejki zadańMakieta poglądowa, dane przykładowe: siatka z wynikami pomiarów kolejki oraz fragment procedury przejęcia zadania. Wynik pomiaru kolejki zadań dwie sesje, poziomy 130 i 160 Pomiar Poziom 130 Poziom 160 Duplikaty, wariant z jedną instrukcją (500 zadań) 0 0 Duplikaty, wariant dwuetapowy (500 zadań) około 490 około 490 Czekanie drugiej sesji z READPAST (ms) 0 1 Czekanie drugiej sesji bez READPAST (ms; blokada przytrzymana 4 s, druga sesja startuje po 1,5 s) 2495 2498 Odczyty logiczne z indeksem filtrowanym 9 9 Odczyty logiczne bez indeksu rzędu 150-190 rzędu 150-190 Przejęcie następnego zadania w jednej instrukcji (nazwy przykładowe) CREATE OR ALTER PROCEDURE dbo.nastepne_zadanie_przyklad @operator varchar(30) AS BEGIN SET NOCOUNT ON; WITH nastepne AS ( SELECT TOP (1) id, nazwa, ranga, status, wlasciciel, przejeto FROM dbo.zadanie_przyklad WITH (UPDLOCK, READPAST, ROWLOCK) WHERE aktywne = 1 AND status = 'nowe' ORDER BY ranga DESC, aktywowano, id ) UPDATE nastepne SET status = 'w_toku', wlasciciel = @operator, przejeto = SYSDATETIME() OUTPUT inserted.id, inserted.nazwa, inserted.ranga, inserted.wlasciciel; END Makieta poglądowa, dane przykładowe
Makieta poglądowa, dane przykładowe. Siatka z wynikami czekania i odczytów oraz blok kodu procedury.

Kolejność według rangi dotyczy tylko zadań, których nie blokuje inna sesja

READPAST pomija wiersze zablokowane, więc druga sesja nie dostaje zadania o najwyższej randze, gdy akurat trzyma je pierwsza. W danych z rangami 3, 3, 2, 2, 1 pierwsza sesja bierze zadanie 1, a druga zadanie 2, czyli drugie o randze 3. W innym teście, z rangami 3, 1, 1, w którym zablokowane było zadanie o randze 3, druga sesja dostała zadanie o randze 1. Kolejność według rangi dotyczy więc zadań, które w danej chwili nie są zablokowane przez inną sesję, i READPAST nie gwarantuje kolejności według rangi.

Dwie sesje biorą zadania z kolejki z READPASTSchemat kolejki zadań z rangami 3, 3, 2, 2, 1. Sesja A trzyma zadanie 1 zablokowane, a sesja B z READPAST pomija je i bierze zadanie 2 bez czekania. Pod spodem porównanie czasu oczekiwania sesji B z READPAST i bez niego. Druga sesja z READPAST pomija zablokowane zadanie Kolejność według rangi dotyczy tylko zadań, których w tej chwili nie blokuje inna sesja. Kolejka, ranga malejąco Zadanie 1 ranga 3 Zadanie 2 ranga 3 Zadanie 3 ranga 2 Zadanie 4 ranga 2 Zadanie 5 ranga 1 Sesja A Przejęła zadanie 1 i trzyma je w otwartej transakcji. bierze zadanie 1 Sesja B z READPAST Pomija zadanie zablokowane przez sesję A i bierze kolejne wolne, bez czekania. pomija bierze zadanie 2 Czas oczekiwania sesji B Sesja A trzyma blokadę 4 s, sesja B zaczyna po 1,5 s. Z READPAST 0 ms (w teście 0 i 1 ms) Bez READPAST około 2,5 s, do zwolnienia blokady start sesji B blokada zwolniona
Schemat: pierwsza sesja trzyma zadanie 1, a druga z READPAST pomija je i bierze zadanie 2 bez czekania.

Przykład (dane wymyślone). W kolejce czeka pięć zadań o rangach 3, 3, 2, 2, 1. Pierwszy operator przejmuje zadanie 1, pierwsze o najwyższej randze, i przez kilka sekund trzyma je w otwartej transakcji. Drugi operator, który prosi o zadanie w tym czasie, z READPAST dostaje od razu zadanie 2, drugie o randze 3, a bez READPAST czeka na zwolnienie blokady.

Pozostałe zadania, o rangach 2, 2, 1, czekają w kolejce. Przy dwuetapowym odczycie i zapisie obaj operatorzy mogą dostać to samo zadanie; ten wyścig opisuje osobny artykuł.

Poziomy SNAPSHOT i SERIALIZABLE kończą READPAST błędem

Procedurę z przykładu uruchomiono na pięciu poziomach izolacji. Trzy z nich przyjmują READPAST, a dwa kończą instrukcję błędem 650.

  • Działa. Poziomy READ UNCOMMITTED, READ COMMITTED oraz REPEATABLE READ.
  • Błąd 650. Poziomy SNAPSHOT i SERIALIZABLE.

Z włączonym READ_COMMITTED_SNAPSHOT wynik jest taki sam jak bez niego: czekanie drugiej sesji wynosi 0 ms z READPAST i około 2,5 s bez, a 500 zadań w wariancie z jedną instrukcją przejęto bez duplikatów.

Ranga liczbowa porządkuje kolejkę, której tekstowy priorytet sortuje się tylko przypadkiem

Gdy priorytet jest tekstem, sortowanie malejące układa etykiety alfabetycznie. W teście cztery etykiety ułożyły się w kolejności Wysoki, Normalny, Niski, Krytyczny, a nowa etykieta trafiła na koniec. Kolejność trzech pierwszych etykiet zgadza się z oczekiwaniem tylko przypadkiem. W przykładzie ranga jest liczbą całkowitą, w której 3 oznacza priorytet wysoki, 2 normalny, a 1 niski, i zapytanie porządkuje zadania według rangi malejąco, potem według czasu aktywacji i identyfikatora.

  • Priorytet tekstowy. Wynik sortowania malejącego: Wysoki, Normalny, Niski, Krytyczny.
  • Ranga liczbowa. Wynik sortowania malejącego: Krytyczny, Wysoki, Normalny, Niski.

Indeks filtrowany ogranicza odczyt do zadań, które czekają na przejęcie

Indeks z pierwszego bloku kodu obejmuje tylko zadania aktywne i oczekujące, a jego klucz ma tę samą kolejność co zapytanie: ranga malejąco, czas aktywacji i identyfikator. Jedno przejęcie z tym indeksem wymagało 9 odczytów logicznych przy 20000 zadań zakończonych i 40 oczekujących. Bez indeksu odczytów było od 156 do 192: 156 w pierwszym biegu i 192 w powtórzonym teście. Liczba odczytów bez indeksu zależy od rozmiaru tabeli. Indeks filtrowany i dwie sesje omawia także artykuł o idempotencji wywołania i indeksie filtrowanym.

Odczyty logiczne jednego przejęcia z indeksem filtrowanym i bez niego
WariantOdczyty logiczne jednego przejęcia
Z indeksem filtrowanym9
Bez indeksuod 156 do 192 (156 w pierwszym biegu, 192 w powtórzonym teście)

Odczyty logiczne przy 20000 zadań zakończonych i 40 oczekujących.

Granice wzorca. READPAST nie gwarantuje kolejności według rangi, co opisuje trzecia sekcja. Przykład nie obsługuje zadania przejętego i nieukończonego: zadanie zostaje w stanie „w toku” z czasem przejęcia, a reguła powrotu do kolejki jest osobną decyzją. Sama klauzula OUTPUT na tabeli z wyzwalaczem kończy się błędem 334 dla procedury z przykładu, a rozwiązanie z OUTPUT INTO opisuje artykuł o znaczniku wysłano.

Podział zadań między procesy zależy od chwili startu, a przerwa 20 ms w wariancie dwuetapowym jest umowna. Wyboru jednego wiersza przez TOP i ORDER BY dotyczą też artykuły o stanie ujemnym przy TOP i o wyborze partii z NULL, a pozostałe teksty o T-SQL zbiera przegląd artykułów o SQL Server.

Pytania do analizy

Przed zastosowaniem wzorca odpowiedz na cztery pytania.

  1. Czy w kolejce dopuszczalne jest przydzielenie zadania o niższej randze przed zadaniem o wyższej, gdy to wyższe jest akurat zablokowane?
  2. Co ma się stać z zadaniem przejętym przez operatora, który się nie zgłosił?
  3. Ilu równoległych konsumentów pracuje na kolejce i jak długo trzyma transakcję jeden z nich?
  4. Które zapytania w bazie wybierają wiersz do przejęcia osobnym odczytem?
FAQ

Najczęstsze pytania o READPAST w kolejce

01

Czy READPAST utrzymuje kolejność według rangi?

Nie. Pomija zablokowane wiersze, więc kolejność dotyczy zadań, które nie są w tej chwili zablokowane przez inną sesję. W teście z rangami 3, 1, 1, w którym zablokowane było zadanie o randze 3, druga sesja dostała zadanie o randze 1.

02

Czy UPDLOCK jest potrzebny razem z READPAST?

W przykładzie użyto obu wskazówek i ROWLOCK. W teście weryfikacyjnym wariant bez UPDLOCK, z READPAST i ROWLOCK, też nie dał duplikatów przy czterech procesach, ale UPDLOCK jawnie wyraża zamiar zablokowania wiersza do zapisu, więc w przykładzie zostaje.

03

Na jakich poziomach izolacji działa READPAST?

W teście READ COMMITTED i REPEATABLE READ, a także READ UNCOMMITTED, działają, a SNAPSHOT i SERIALIZABLE kończą się błędem 650. Z włączonym READ_COMMITTED_SNAPSHOT wynik jest taki sam jak bez niego.

04

Co z zadaniem przejętym i nieukończonym?

Przykład tego nie obsługuje: zadanie zostaje w stanie „w toku” z czasem przejęcia. Reguła powrotu do kolejki jest osobną decyzją.

05

Czy sam OUTPUT wystarczy, gdy tabela ma wyzwalacz?

Nie. W teście procedura z przykładu na tabeli z wyzwalaczem kończy się błędem 334, a rozwiązaniem jest OUTPUT INTO, opisane w artykule o znaczniku wysłano.

Słownik pojęć

Słownik pojęć

Pojęcia z procedury i pomiarów opisanych w artykule.

KKolejka zadań
tabela, z której konsumenci biorą zadania w ustalonej kolejności.
RREADPAST
wskazówka tabeli, która pomija wiersze zablokowane przez inne sesje zamiast czekać.
UUPDLOCK
wskazówka, która ustawia blokadę aktualizacji już przy odczycie wiersza.
OOUTPUT
klauzula polecenia UPDATE, która zwraca zmienione wiersze w tym samym kroku.
RRanga priorytetu
liczba, według której porządkuje się kolejkę (w przykładzie 3 jest najwyższa).
IIndeks filtrowany
indeks tylko dla wierszy spełniających warunek, tu zadań aktywnych i oczekujących.

Zobacz to w praktyce

Sprawdź, jak systemy SoftwareStudio oparte na MS SQL Server wspierają ten obszar Twojej firmy.