it-swarm-eu.dev

Sloučit prohlášení deadlocking sám

Mám následující postup (SQL Server 2008 R2):

create procedure usp_SaveCompanyUserData
    @companyId bigint,
    @userId bigint,
    @dataTable tt_CoUserdata readonly
as
begin

    set nocount, xact_abort on;

    merge CompanyUser with (holdlock) as r
    using (
        select 
            @companyId as CompanyId, 
            @userId as UserId, 
            MyKey, 
            MyValue
        from @dataTable) as newData
    on r.CompanyId = newData.CompanyId
        and r.UserId = newData.UserId
        and r.MyKey = newData.MyKey
    when not matched then
        insert (CompanyId, UserId, MyKey, MyValue) values
        (@companyId, @userId, newData.MyKey, newData.MyValue);

end;

CompanyId, UserId, MyKey tvoří složený klíč pro cílovou tabulku. CompanyId je cizí klíč k nadřazené tabulce. Na CompanyId asc, UserId asc.

Volá se z mnoha různých podprocesů a neustále získávám uváznutí mezi různými procesy, které nazývají totožné prohlášení. Pochopil jsem, že "s (holdlock)" bylo nutné, aby se zabránilo chybám v závodě vložit/aktualizovat.

Předpokládám, že dva různé podprocesy jsou uzamykací řádky (nebo stránky) v různých pořadích, když ověřují omezení, a tedy jsou zablokování.

Je to správný předpoklad?

Jaký je nejlepší způsob, jak tuto situaci vyřešit (tj. Žádné zablokování, minimální dopad na výkon s více vlákny)?

Query Plan Image (Pokud si prohlédnete obrázek na nové kartě, je čitelný. Omlouváme se za jeho malou velikost.)

  • V @datatable je maximálně 28 řádků.
  • Sledoval jsem kód zpět a nikde nevidím, že zde zahajujeme transakci.
  • Cizí klíč je nastaven na kaskádu pouze při mazání a z rodičovské tabulky nedošlo k žádnému odstranění.
22
Sako73

Dobře, poté, co jsem se několikrát podíval na všechno, myslím, že tvůj základní předpoklad byl správný. Pravděpodobně se zde děje toto:

  1. Část MATCH v MERGE kontroluje index na shody a tyto řádky/stránky uzamkne.

  2. Pokud má řádek bez shody, pokusí se nejprve vložit nový řádek indexu, aby si vyžádal zámek zápisu na řádku/stránce ...

Pokud však jiný uživatel také přistoupil ke kroku 1 na stejném řádku/stránce, bude první uživatel zablokován z aktualizace a ...

Pokud také druhý uživatel potřebuje vložit na stejnou stránku, je v mrtvém bodě.

AFAIK, existuje jen jeden (jednoduchý) způsob, jak si být stoprocentně jistý, že s tímto postupem nemůžete dojít k zablokování, a to by bylo přidání nápovědy TABLOCKX k MERGE, ale to by pravděpodobně mělo opravdu špatný dopad na výkon.

Je možné, že přidání tipu TABLOCK místo toho by stačilo k vyřešení problému, aniž by to mělo velký dopad na váš výkon.

Nakonec můžete také zkusit přidat PAGLOCK, XLOCK nebo oba PAGLOCK a XLOCK. Opět platí, že možná práce a výkon možná nebude příliš hrozné. Musíte to zkusit, abyste to viděli.

12
RBarryYoung

Nebyl by problém, pokud by proměnná tabulky obsahovala pouze jednu hodnotu. U více řádků existuje nová možnost zablokování. Předpokládejme, že dva souběžné procesy (A a B) probíhají s tabulkovými proměnnými obsahujícími (1, 2) a (2, 1) pro stejnou společnost.

Proces A přečte cíl, nenajde žádný řádek a vloží hodnotu '1'. Drží exkluzivní zámek řádku na hodnotě „1“. Proces B přečte cíl, nenajde žádný řádek a vloží hodnotu '2'. Drží exkluzivní zámek řádku na hodnotě „2“.

Nyní proces A musí zpracovat řádek 2 a proces B musí zpracovat řádek 1. Žádný proces nemůže dosáhnout pokroku, protože vyžaduje zámek, který je nekompatibilní s exkluzivním zámkem drženým jiným procesem.

Aby se zabránilo zablokování s více řádky, musí být řádky zpracovány (a přístup k tabulkám) ve stejném pořadí pokaždé. Proměnná tabulky v plánu provádění zobrazeném v otázce je halda, takže řádky nemají žádné vnitřní pořadí (je docela pravděpodobné, že budou čteny v pořadí vkládání, i když to není zaručeno):

Existing plan

Nedostatek konzistentního pořadí zpracování řádků vede přímo k možnosti zablokování. Druhým hlediskem je, že absence klíčové záruky jedinečnosti znamená, že stolní cívka je nezbytná pro zajištění správné ochrany Halloweenu. Cívka je dychtivá cívka, což znamená všechny řádky jsou zapsány do tempdb před opětovným načtením a přehráním pro operátora Insert.

Předefinování TYPE proměnné tabulky tak, aby zahrnovala seskupený PRIMARY KEY:

DROP TYPE dbo.CoUserData;

CREATE TYPE dbo.CoUserData
AS TABLE
(
    MyKey   integer NOT NULL PRIMARY KEY CLUSTERED,
    MyValue integer NOT NULL
);

Plán provádění nyní ukazuje skenování seskupeného indexu a záruka jedinečnosti znamená, že optimalizátor je schopen bezpečně odebrat zařazování tabulky:

With primary key

V testech s 5000 iteracemi příkazu MERGE na 128 vláknech nedošlo k žádné zablokování s proměnnou seskupené tabulky. Měl bych zdůraznit, že je to pouze na základě pozorování; proměnná seskupené tabulky by také mohla ( technicky ) produkovat své řady v různých řádech, ale šance na konzistentní pořadí jsou velmi značně vylepšeny. Pozorované chování bude samozřejmě nutné znovu testovat pro každou novou kumulativní aktualizaci, aktualizaci Service Pack nebo novou verzi SQL Serveru.

V případě, že definici proměnné tabulky nelze změnit, existuje další alternativa:

MERGE dbo.CompanyUser AS R
USING 
    (SELECT DISTINCT MyKey, MyValue FROM @DataTable) AS NewData ON
    R.CompanyId = @CompanyID
    AND R.UserID = @UserID
    AND R.MyKey = NewData.MyKey
WHEN NOT MATCHED THEN 
    INSERT 
        (CompanyID, UserID, MyKey, MyValue) 
    VALUES
        (@CompanyID, @UserID, NewData.MyKey, NewData.MyValue)
OPTION (ORDER GROUP);

Tím se také dosáhne eliminace cívky (a konzistence řádků) za cenu zavedení explicitního druhu:

Sort plan

Tento plán také nevyprodukoval žádné slepé uličky pomocí stejného testu. Reprodukční skript níže:

CREATE TYPE dbo.CoUserData
AS TABLE
(
    MyKey   integer NOT NULL /* PRIMARY KEY */,
    MyValue integer NOT NULL
);
GO
CREATE TABLE dbo.Company
(
    CompanyID   integer NOT NULL

    CONSTRAINT PK_Company
        PRIMARY KEY (CompanyID)
);
GO
CREATE TABLE dbo.CompanyUser
(
    CompanyID   integer NOT NULL,
    UserID      integer NOT NULL,
    MyKey       integer NOT NULL,
    MyValue     integer NOT NULL

    CONSTRAINT PK_CompanyUser
        PRIMARY KEY CLUSTERED
            (CompanyID, UserID, MyKey),

    FOREIGN KEY (CompanyID)
        REFERENCES dbo.Company (CompanyID),
);
GO
CREATE NONCLUSTERED INDEX nc1
ON dbo.CompanyUser (CompanyID, UserID);
GO
INSERT dbo.Company (CompanyID) VALUES (1);
GO
DECLARE 
    @DataTable AS dbo.CoUserData,
    @CompanyID integer = 1,
    @UserID integer = 1;

INSERT @DataTable
SELECT TOP (10)
    V.MyKey,
    V.MyValue
FROM
(
    VALUES
        (1, 1),
        (2, 2),
        (3, 3),
        (4, 4),
        (5, 5),
        (6, 6),
        (7, 7),
        (8, 8),
        (9, 9)
) AS V (MyKey, MyValue)
ORDER BY NEWID();

BEGIN TRANSACTION;

    -- Test MERGE statement here

ROLLBACK TRANSACTION;
31
Paul White 9

Myslím, že SQL_Kiwi poskytla velmi dobrou analýzu. Pokud potřebujete problém vyřešit v databázi, měli byste se řídit jeho návrhem. Samozřejmě je třeba znovu otestovat, že stále funguje pro vás pokaždé, když upgradujete, použijete aktualizaci Service Pack nebo přidáte/změníte index nebo indexované zobrazení.

Existují další tři alternativy:

  1. Servery můžete serializovat tak, aby se nekolidovaly: na začátku transakce můžete vyvolat sp_getapplock a získat exkluzivní zámek před provedením MERGE. Samozřejmě to musíte ještě stresovat.

  2. Všechny vložky můžete zpracovat jedním vláknem, takže váš aplikační server zpracovává souběžnost.

  3. Můžete automaticky opakovat po zablokování - to může být nejpomalejší přístup, pokud je souběžnost vysoká.

Ať tak či onak, pouze vy můžete určit dopad vašeho řešení na výkon.

Obvykle v našem systému nemáme uváznutí na mrtvém bodě, i když máme pro ně velký potenciál. V roce 2011 jsme udělali chybu v jednom rozmístění a během několika hodin jsme měli několik desítek mrtvých míst, všichni sledovali stejný scénář. Brzy jsem to napravil a to byly všechny zablokování roku.

V našem systému většinou využíváme přístup 1. Funguje to pro nás opravdu dobře.

8
A-K