Unire la dichiarazione deadlock stesso


22

Ho la seguente procedura (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 formano la chiave composita per la tabella di destinazione. CompanyId è una chiave esterna per una tabella padre. Inoltre, è attivo un indice non cluster CompanyId asc, UserId asc.

È chiamato da molti thread diversi e sto costantemente ottenendo blocchi tra processi diversi che chiamano questa stessa affermazione. La mia comprensione era che "with (holdlock)" era necessario per evitare errori di inserimento / aggiornamento delle condizioni di gara.

Presumo che due thread diversi stiano bloccando le righe (o le pagine) in ordini diversi quando convalidano i vincoli e quindi sono deadlock.

È un presupposto corretto?

Qual è il modo migliore per risolvere questa situazione (ovvero senza deadlock, impatto minimo sulle prestazioni multi-thread)?

Immagine del piano di query (Se visualizzi l'immagine in una nuova scheda, è leggibile. Ci scusiamo per le dimensioni ridotte.)

  • Ci sono al massimo 28 righe nel @datatable.
  • Ho rintracciato il codice e non riesco a vedere da nessuna parte che iniziamo una transazione qui.
  • La chiave esterna è impostata per essere sovrapposta solo all'eliminazione e non vi sono state cancellazioni dalla tabella padre.

Risposte:


12

OK, dopo aver esaminato tutto un paio di volte, penso che il tuo presupposto di base sia stato corretto. Quello che probabilmente sta succedendo qui è che:

  1. La parte MATCH di MERGE controlla la corrispondenza dell'indice, bloccando la lettura di tali righe / pagine mentre procede.

  2. Quando ha una riga senza una corrispondenza, proverà a inserire prima la nuova riga dell'indice, quindi richiederà un blocco di scrittura riga / pagina ...

Ma se un altro utente ha anche ottenuto il passaggio 1 sulla stessa riga / pagina, il primo utente verrà bloccato dall'aggiornamento e ...

Se anche il secondo utente deve inserire nella stessa pagina, è in un deadlock.

AFAIK, c'è solo un modo (semplice) per essere sicuri al 100% che non è possibile ottenere un deadlock con questa procedura e che sarebbe aggiungere un suggerimento TABLOCKX a MERGE, ma ciò avrebbe probabilmente un impatto davvero negativo sulle prestazioni.

È possibile che l'aggiunta di un suggerimento TABLOCK sia sufficiente per risolvere il problema senza influire notevolmente sulle prestazioni.

Infine, puoi anche provare ad aggiungere PAGLOCK, XLOCK o entrambi PAGLOCK e XLOCK. Anche in questo caso potrebbe funzionare e le prestazioni potrebbero non essere troppo terribili. Dovrai provarlo per vedere.


Pensi che il livello di isolamento dell'istantanea (versioning delle righe) possa essere utile qui?
Mikael Eriksson,

Può essere. Oppure può trasformare le eccezioni deadlock in eccezioni di concorrenza.
RBarryYoung,

2
La specifica del suggerimento TABLOCK su una tabella che è la destinazione di un'istruzione INSERT ha lo stesso effetto della specifica del suggerimento TABLOCKX. (Fonte: msdn.microsoft.com/en-us/library/bb510625.aspx )
tuespetre,

31

Non ci sarebbe un problema se la variabile della tabella contenesse un solo valore. Con più righe, esiste una nuova possibilità di deadlock. Supponiamo che due processi simultanei (A e B) vengano eseguiti con variabili di tabella contenenti (1, 2) e (2, 1) per la stessa azienda.

Il processo A legge la destinazione, non trova alcuna riga e inserisce il valore "1". Contiene un blocco riga esclusivo sul valore "1". Il processo B legge la destinazione, non trova alcuna riga e inserisce il valore "2". Contiene un blocco riga esclusivo sul valore "2".

Ora il processo A deve elaborare la riga 2 e il processo B deve elaborare la riga 1. Nessuno dei due processi può avanzare perché richiede un blocco incompatibile con il blocco esclusivo detenuto dall'altro processo.

Per evitare deadlock con più righe, le righe devono essere elaborate (e le tabelle accessibili) nello stesso ordine ogni volta . La variabile di tabella nel piano di esecuzione mostrato nella domanda è un heap, quindi le righe non hanno un ordine intrinseco (è molto probabile che vengano lette in ordine di inserimento, sebbene ciò non sia garantito):

Piano esistente

La mancanza di un ordine di elaborazione delle righe coerente porta direttamente all'opportunità di deadlock. Una seconda considerazione è che la mancanza di una garanzia di unicità fondamentale significa che una bobina da tavolo è necessaria per fornire una corretta protezione di Halloween. Lo spool è uno spool desideroso, il che significa che tutte le righe vengono scritte su un tavolo di lavoro tempdb prima di essere rilette e riprodotte per l'operatore Inserisci.

Ridefinire la TYPEvariabile della tabella per includere un cluster PRIMARY KEY:

DROP TYPE dbo.CoUserData;

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

Il piano di esecuzione ora mostra una scansione dell'indice cluster e la garanzia di unicità significa che l'ottimizzatore è in grado di rimuovere in sicurezza lo spool della tabella:

Con chiave primaria

Nei test con 5000 iterazioni MERGEdell'istruzione su 128 thread, non si sono verificati deadlock con la variabile di tabella cluster. Dovrei sottolineare che questo è solo sulla base dell'osservazione; la variabile di tabella raggruppata potrebbe anche ( tecnicamente ) produrre le sue righe in una varietà di ordini, ma le possibilità di un ordine coerente sono notevolmente migliorate. Il comportamento osservato dovrebbe essere nuovamente testato per ogni nuovo aggiornamento cumulativo, service pack o nuova versione di SQL Server, ovviamente.

Nel caso in cui la definizione della variabile di tabella non possa essere modificata, esiste un'altra 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);

Ciò consente anche di eliminare lo spool (e la coerenza dell'ordine delle righe) al costo di introdurre un ordinamento esplicito:

Piano di ordinamento

Questo piano inoltre non ha prodotto deadlock utilizzando lo stesso test. Script di riproduzione di seguito:

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;

8

Penso che SQL_Kiwi abbia fornito un'ottima analisi. Se è necessario risolvere il problema nel database, è necessario seguire il suo suggerimento. Ovviamente devi ripetere il test che funziona ancora per te ogni volta che aggiorni, applichi un service pack o aggiungi / cambi un indice o una vista indicizzata.

Esistono altre tre alternative:

  1. Puoi serializzare i tuoi inserti in modo che non si scontrino: puoi invocare sp_getapplock all'inizio della transazione e acquisire un blocco esclusivo prima di eseguire MERGE. Ovviamente devi ancora stressarlo.

  2. Puoi avere un thread che gestisce tutti i tuoi inserimenti, in modo che il tuo server delle applicazioni gestisca la concorrenza.

  3. Puoi riprovare automaticamente dopo i deadlock: questo potrebbe essere l'approccio più lento se la concorrenza è alta.

In entrambi i casi, solo tu puoi determinare l'impatto della tua soluzione sulle prestazioni.

In genere non abbiamo alcun deadlock nel nostro sistema, anche se abbiamo un grande potenziale per averli. Nel 2011 abbiamo commesso un errore in una distribuzione e in poche ore si sono verificati mezza dozzina di deadlock, tutti seguendo lo stesso scenario. L'ho risolto presto e questo era tutto il punto morto dell'anno.

Utilizziamo principalmente l'approccio 1 nel nostro sistema. Funziona davvero bene per noi.


-1

Un altro possibile approccio - ho trovato Merge a volte presentare problemi di blocco e prestazioni - potrebbe valere la pena giocare con l'opzione di query Option (MaxDop x)

Nel lontano e distante SQL Server aveva un'opzione Inserisci blocco livello riga - ma questo sembra essere morto, tuttavia un PK cluster con un'identità dovrebbe rendere gli inserimenti puliti.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.