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)?
(Pokud si prohlédnete obrázek na nové kartě, je čitelný. Omlouváme se za jeho malou velikost.)
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:
Část MATCH v MERGE kontroluje index na shody a tyto řádky/stránky uzamkne.
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.
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):
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:
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:
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;
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:
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.
Všechny vložky můžete zpracovat jedním vláknem, takže váš aplikační server zpracovává souběžnost.
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.