Gestione dell'accesso simultaneo a una tabella di chiavi senza deadlock in SQL Server


32

Ho una tabella che viene utilizzata da un'applicazione legacy come sostituto dei IDENTITYcampi in varie altre tabelle.

Ogni riga nella tabella memorizza l'ultimo ID utilizzato LastIDper il campo indicato IDName.

Occasionalmente il proc memorizzato ottiene un deadlock - credo di aver creato un appropriato gestore degli errori; tuttavia sono interessato a vedere se questa metodologia funziona come penso, o se abbaio qui l'albero sbagliato.

Sono abbastanza certo che ci dovrebbe essere un modo per accedere a questa tabella senza alcun deadlock.

Il database stesso è configurato con READ_COMMITTED_SNAPSHOT = 1.

Innanzitutto, ecco la tabella:

CREATE TABLE [dbo].[tblIDs](
    [IDListID] [int] NOT NULL 
        CONSTRAINT PK_tblIDs 
        PRIMARY KEY CLUSTERED 
        IDENTITY(1,1) ,
    [IDName] [nvarchar](255) NULL,
    [LastID] [int] NULL,
);

E l'indice non cluster sul IDNamecampo:

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

Alcuni dati di esempio:

INSERT INTO tblIDs (IDName, LastID) 
    VALUES ('SomeTestID', 1);
INSERT INTO tblIDs (IDName, LastID) 
    VALUES ('SomeOtherTestID', 1);
GO

La procedura memorizzata utilizzata per aggiornare i valori memorizzati nella tabella e restituire l'ID successivo:

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

Esecuzioni di esempio del proc memorizzato:

EXEC GetNextID 'SomeTestID';

NewID
2

EXEC GetNextID 'SomeTestID';

NewID
3

EXEC GetNextID 'SomeOtherTestID';

NewID
2

MODIFICARE:

Ho aggiunto un nuovo indice, poiché l'indice esistente IX_tblIDs_Name non viene utilizzato dall'SP; Presumo che il Query Processor stia utilizzando l'indice cluster poiché ha bisogno del valore memorizzato in LastID. Ad ogni modo, questo indice viene utilizzato dal piano di esecuzione effettivo:

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);

EDIT # 2:

Ho seguito il consiglio dato da @AaronBertrand e l'ho modificato leggermente. L'idea generale qui è quella di affinare l'affermazione per eliminare il blocco non necessario e, nel complesso, per rendere l'SP più efficiente.

Il codice seguente sostituisce il codice sopra da BEGIN TRANSACTIONa 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;

Dal momento che il nostro codice non aggiunge mai un record a questa tabella con 0 in, LastIDpossiamo supporre che se @NewID è 1, l'intenzione è di aggiungere un nuovo ID all'elenco, altrimenti stiamo aggiornando una riga esistente nell'elenco.


Il modo in cui è stato configurato il database per supportare RCSI è irrilevante. Stai intenzionalmente aumentando fino a SERIALIZABLEqui.
Aaron Bertrand

si, volevo solo aggiungere tutte le informazioni rilevanti. Sono contento che tu stia confermando che è irrilevante!
Max Vernon,

è molto facile che sp_getapplock diventi una vittima del deadlock, ma non se si avvia la transazione, chiamare sp_getapplock una volta per acquisire un blocco esclusivo e procedere con la modifica.
AK,

1
IDName è unico? Quindi consiglio "crea un indice non cluster univoco ". Tuttavia, se sono necessari valori Null, è necessario filtrare anche l'indice .
crokusek,

Risposte:


15

Innanzitutto, eviterei di fare un giro di andata e ritorno nel database per ogni valore. Ad esempio, se l'applicazione sa che necessita di 20 nuovi ID, non effettuare 20 round trip. Effettuare una sola chiamata di procedura memorizzata e incrementare il contatore di 20. Inoltre, potrebbe essere meglio suddividere la tabella in più tabelle.

È possibile evitare del tutto i deadlock. Non ho alcun deadlock nel mio sistema. Esistono diversi modi per farlo. Mostrerò come utilizzare sp_getapplock per eliminare i deadlock. Non ho idea se questo funzionerà per te, perché SQL Server è un codice sorgente chiuso, quindi non riesco a vedere il codice sorgente e come tale non so se ho testato tutti i casi possibili.

Di seguito viene descritto ciò che funziona per me. YMMV.

Innanzitutto, iniziamo con uno scenario in cui otteniamo sempre una notevole quantità di deadlock. Secondo, useremo sp_getapplock per eliminarli. Il punto più importante qui è sottoporre a stress test la tua soluzione. La tua soluzione potrebbe essere diversa, ma devi esporla a una concorrenza elevata, come dimostrerò più avanti.

Prerequisiti

Cerchiamo di impostare una tabella con alcuni dati di test:

CREATE TABLE dbo.Numbers(n INT NOT NULL PRIMARY KEY); 
GO 

INSERT INTO dbo.Numbers 
    ( n ) 
        VALUES  ( 1 ); 
GO 
DECLARE @i INT; 
    SET @i=0; 
WHILE @i<21  
    BEGIN 
    INSERT INTO dbo.Numbers 
        ( n ) 
        SELECT n + POWER(2, @i) 
        FROM dbo.Numbers; 
    SET @i = @i + 1; 
    END;  
GO

SELECT n AS ID, n AS Key1, n AS Key2, 0 AS Counter1, 0 AS Counter2
INTO dbo.DeadlockTest FROM dbo.Numbers
GO

ALTER TABLE dbo.DeadlockTest ADD CONSTRAINT PK_DeadlockTest PRIMARY KEY(ID);
GO

CREATE INDEX DeadlockTestKey1 ON dbo.DeadlockTest(Key1);
GO

CREATE INDEX DeadlockTestKey2 ON dbo.DeadlockTest(Key2);
GO

È probabile che le seguenti due procedure si abbraccino in un deadlock:

CREATE PROCEDURE dbo.UpdateCounter1 @Key1 INT
AS
SET NOCOUNT ON ;
SET XACT_ABORT ON;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN TRANSACTION ;
UPDATE dbo.DeadlockTest SET Counter1=Counter1+1 WHERE Key1=@Key1;
SET @Key1=@Key1-10000;
UPDATE dbo.DeadlockTest SET Counter1=Counter1+1 WHERE Key1=@Key1;
COMMIT;
GO

CREATE PROCEDURE dbo.UpdateCounter2 @Key2 INT
AS
SET NOCOUNT ON ;
SET XACT_ABORT ON;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN TRANSACTION ;
SET @Key2=@Key2-10000;
UPDATE dbo.DeadlockTest SET Counter2=Counter2+1 WHERE Key2=@Key2;
SET @Key2=@Key2+10000;
UPDATE dbo.DeadlockTest SET Counter2=Counter2+1 WHERE Key2=@Key2;
COMMIT;
GO

Riproduzione di deadlock

I loop seguenti dovrebbero riprodurre più di 20 deadlock ogni volta che li esegui. Se ottieni meno di 20, aumenta il numero di iterazioni.

In una scheda, esegui questo;

DECLARE @i INT, @DeadlockCount INT;
SELECT @i=0, @DeadlockCount=0;

WHILE @i<5000 BEGIN ;
  BEGIN TRY 
    EXEC dbo.UpdateCounter1 @Key1=123456;
  END TRY
  BEGIN CATCH
    SET @DeadlockCount = @DeadlockCount + 1;
    ROLLBACK;
  END CATCH ;
  SET @i = @i + 1;
END;
SELECT 'Deadlocks caught: ', @DeadlockCount ;

In un'altra scheda, esegui questo script.

DECLARE @i INT, @DeadlockCount INT;
SELECT @i=0, @DeadlockCount=0;

WHILE @i<5000 BEGIN ;
  BEGIN TRY 
    EXEC dbo.UpdateCounter2 @Key2=123456;
  END TRY
  BEGIN CATCH
    SET @DeadlockCount = @DeadlockCount + 1;
    ROLLBACK;
  END CATCH ;
  SET @i = @i + 1;
END;
SELECT 'Deadlocks caught: ', @DeadlockCount ;

Assicurati di iniziare entrambi entro un paio di secondi.

Utilizzo di sp_getapplock per eliminare i deadlock

Modifica entrambe le procedure, riesegui il ciclo e vedi che non hai più deadlock:

ALTER PROCEDURE dbo.UpdateCounter1 @Key1 INT
AS
SET NOCOUNT ON ;
SET XACT_ABORT ON;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN TRANSACTION ;
EXEC sp_getapplock @Resource='DeadlockTest', @LockMode='Exclusive';
UPDATE dbo.DeadlockTest SET Counter1=Counter1+1 WHERE Key1=@Key1;
SET @Key1=@Key1-10000;
UPDATE dbo.DeadlockTest SET Counter1=Counter1+1 WHERE Key1=@Key1;
COMMIT;
GO

ALTER PROCEDURE dbo.UpdateCounter2 @Key2 INT
AS
SET NOCOUNT ON ;
SET XACT_ABORT ON;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN TRANSACTION ;
EXEC sp_getapplock @Resource='DeadlockTest', @LockMode='Exclusive';
SET @Key2=@Key2-10000;
UPDATE dbo.DeadlockTest SET Counter2=Counter2+1 WHERE Key2=@Key2;
SET @Key2=@Key2+10000;
UPDATE dbo.DeadlockTest SET Counter2=Counter2+1 WHERE Key2=@Key2;
COMMIT;
GO

Utilizzo di una tabella con una riga per eliminare i deadlock

Invece di invocare sp_getapplock, possiamo modificare la seguente tabella:

CREATE TABLE dbo.DeadlockTestMutex(
ID INT NOT NULL,
CONSTRAINT PK_DeadlockTestMutex PRIMARY KEY(ID),
Toggle INT NOT NULL);
GO

INSERT INTO dbo.DeadlockTestMutex(ID, Toggle)
VALUES(1,0);

Dopo aver creato e compilato questa tabella, possiamo sostituire la seguente riga

EXEC sp_getapplock @Resource='DeadlockTest', @LockMode='Exclusive';

con questo, in entrambe le procedure:

UPDATE dbo.DeadlockTestMutex SET Toggle = 1 - Toggle WHERE ID = 1;

Puoi rieseguire lo stress test e vedere di persona che non abbiamo deadlock.

Conclusione

Come abbiamo visto, sp_getapplock può essere utilizzato per serializzare l'accesso ad altre risorse. Come tale, può essere utilizzato per eliminare i deadlock.

Naturalmente, questo può rallentare significativamente le modifiche. Per risolverlo, dobbiamo scegliere la granularità giusta per il blocco esclusivo e, quando possibile, lavorare con set anziché con singole righe.

Prima di utilizzare questo approccio è necessario sottoporlo a stress test. Innanzitutto, devi assicurarti di ottenere almeno una dozzina di deadlock con il tuo approccio originale. Secondo, non dovresti ottenere deadlock quando riesegui lo stesso script di repro usando la stored procedure modificata.

In generale, non penso che ci sia un buon modo per determinare se il tuo T-SQL è al sicuro da deadlock semplicemente guardandolo o guardando il piano di esecuzione. L'IMO l'unico modo per determinare se il tuo codice è soggetto a deadlock è quello di esporlo a una concorrenza elevata.

Buona fortuna con l'eliminazione dei deadlock! Non abbiamo alcun deadlock nel nostro sistema, il che è ottimo per il nostro equilibrio tra lavoro e vita privata.


2
+1 come sp_getapplock è uno strumento utile che non è ben noto. Dato un 'orribile pasticcio che potrebbe richiedere del tempo per essere separato, è un trucco utile serializzare un processo che è deadlock. Ma dovrebbe essere la prima scelta per un caso come questo che è facilmente comprensibile e può (forse dovrebbe) essere gestito da meccanismi di blocco standard?
Mark Storey-Smith,

2
@ MarkStorey-Smith È la mia prima scelta perché ho studiato e stressato una sola volta, e posso riutilizzarlo in qualsiasi situazione: la serializzazione è già avvenuta, quindi tutto ciò che accade dopo sp_getapplock non influisce sul risultato. Con i meccanismi di blocco standard, non posso mai esserne così sicuro: l'aggiunta di un indice o il semplice ottenimento di un altro piano di esecuzione possono causare deadlock in cui prima non esistevano. Chiedimi come lo so.
AK

Immagino che mi manchi qualcosa di ovvio, ma come si fa a UPDATE dbo.DeadlockTestMutex SET Toggle = 1 - Toggle WHERE ID = 1;prevenire i deadlock?
Dale K,

9

L'uso del XLOCKsuggerimento sul tuo SELECTapproccio o quanto segue UPDATEdovrebbe essere immune a questo tipo di deadlock:

DECLARE @Output TABLE ([NewId] INT);
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

BEGIN TRANSACTION;

UPDATE
    dbo.tblIDs WITH (XLOCK)
SET 
    LastID = LastID + 1
OUTPUT
    INSERTED.[LastId] INTO @Output
WHERE
    IDName = @IDName;

IF(@@ROWCOUNT = 1)
BEGIN
    SELECT @NewId = [NewId] FROM @Output;
END
ELSE
BEGIN
    SET @NewId = 1;

    INSERT dbo.tblIDs
        (IDName, LastID)
    VALUES
        (@IDName, @NewId);
END

SELECT [NewId] = @NewId ;

COMMIT TRANSACTION;

Tornerà con un paio di altre varianti (se non battuto ad esso!).


Mentre XLOCKimpedirà a un contatore esistente di essere aggiornato da più connessioni, non avresti bisogno di un TABLOCKXper impedire a più connessioni di aggiungere lo stesso nuovo contatore?
Dale K,

1
@DaleBurrell No, avresti PK o vincolo univoco su IDName.
Mark Storey-Smith,

7

Mike Defehr mi ha mostrato un modo elegante per farlo in un modo molto leggero:

ALTER PROCEDURE [dbo].[GetNextID](
    @IDName nvarchar(255)
)
AS
BEGIN
    /*
        Description:    Increments and returns the LastID value from tblIDs for a given IDName
        Author:         Max Vernon / Mike Defehr
        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
            UPDATE dbo.tblIDs 
            SET @NewID = LastID = LastID + 1 
            WHERE IDName = @IDName;

            IF @NewID IS NULL
            BEGIN
                SET @NewID = 1;
                INSERT INTO tblIDs (IDName, LastID) VALUES (@IDName, @NewID);
            END
            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
        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

(Per completezza, ecco la tabella associata al proc memorizzato)

CREATE TABLE [dbo].[tblIDs]
(
    IDName nvarchar(255) NOT NULL,
    LastID int NULL,
    CONSTRAINT [PK_tblIDs] PRIMARY KEY CLUSTERED 
    (
        [IDName] ASC
    ) WITH 
    (
        PAD_INDEX = OFF
        , STATISTICS_NORECOMPUTE = OFF
        , IGNORE_DUP_KEY = OFF
        , ALLOW_ROW_LOCKS = ON
        , ALLOW_PAGE_LOCKS = ON
        , FILLFACTOR = 100
    ) 
);
GO

Questo è il piano di esecuzione per l'ultima versione:

inserisci qui la descrizione dell'immagine

E questo è il piano di esecuzione per la versione originale (suscettibile di deadlock):

inserisci qui la descrizione dell'immagine

Chiaramente, la nuova versione vince!

Per confronto, la versione intermedia con (XLOCK)etc, produce il seguente piano:

inserisci qui la descrizione dell'immagine

Direi che è una vittoria! Grazie per l'aiuto di tutti!


2
Dovrebbe davvero funzionare ma stai usando SERIALIZZABILE dove non è applicabile. Le righe fantasma non possono esistere qui, quindi perché usare un livello di isolamento esistente per prevenirle? Inoltre, se qualcuno chiama la procedura da un'altra o da una connessione in cui è stata avviata una transazione esterna, qualsiasi ulteriore azione avviata procederà su SERIALIZZABILE. Questo può diventare disordinato.
Mark Storey-Smith,

2
SERIALIZABLEnon esiste per prevenire i fantasmi. Esiste per fornire semantica di isolamento serializzabile , ovvero lo stesso effetto persistente sul database come se le transazioni in questione fossero state eseguite in serie in un ordine non specificato.
Paul White dice GoFundMonica

6

Non per rubare il tuono di Mark Storey-Smith, ma è su qualcosa con il suo post sopra (che per inciso ha ricevuto il maggior numero di voti). Il consiglio che ho dato a Max era incentrato sul costrutto "UPDATE set @variable = column = column + value" che trovo davvero interessante, ma penso che potrebbe non essere documentato (deve essere supportato, anche se perché è lì appositamente per il TCP parametri di riferimento).

Ecco una variazione della risposta di Mark: poiché stai restituendo il nuovo valore ID come recordset, puoi eliminare completamente la variabile scalare, non dovrebbe essere necessaria alcuna transazione esplicita e sarei d'accordo che non è necessario fare confusione con i livelli di isolamento anche. Il risultato è molto pulito e piuttosto liscio ...

ALTER PROC [dbo].[GetNextID]
  @IDName nvarchar(255)
  AS
BEGIN
SET NOCOUNT ON;

DECLARE @Output TABLE ([NewID] INT);

UPDATE dbo.tblIDs SET LastID = LastID + 1
OUTPUT inserted.[LastId] INTO @Output
WHERE IDName = @IDName;

IF(@@ROWCOUNT = 1)
    SELECT [NewID] FROM @Output;
ELSE
    INSERT dbo.tblIDs (IDName, LastID)
    OUTPUT INSERTED.LastID AS [NewID]
    VALUES (@IDName,1);
END

3
D'accordo, questo dovrebbe essere immune allo stallo, ma è soggetto a una condizione di competizione sull'inserto, se si omette la transazione.
Mark Storey-Smith,

4

L'anno scorso ho risolto un deadlock simile in un sistema modificando questo:

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;

A questa:

UPDATE tblIDs SET LastID = @NewID WHERE IDName = @IDName;
IF @@ROWCOUNT = 0
BEGIN
  INSERT ...
END

In generale, selezionare un COUNTgiusto per determinare la presenza o l'assenza è piuttosto dispendioso. In questo caso, dato che è 0 o 1, non è come se fosse un sacco di lavoro, ma (a) quell'abitudine può sfociare in altri casi in cui sarà molto più costoso (in quei casi, utilizzare al IF NOT EXISTSposto di IF COUNT() = 0) e (b) la scansione aggiuntiva è completamente inutile. IlUPDATE esegue sostanzialmente lo stesso controllo.

Inoltre, questo mi sembra un odore di codice serio:

SET @NewID = COALESCE((SELECT LastID FROM tblIDs WHERE IDName = @IDName),0)+1;

Qual è il punto qui? Perché non usare semplicemente una colonna di identità o derivare quella sequenza usando ROW_NUMBER()al momento della query?


La maggior parte delle tabelle che abbiamo stanno usando un IDENTITY. Questa tabella supporta alcuni codici legacy scritti in MS Access che sarebbero abbastanza coinvolti per il retrofit. La SET @NewID=riga semplicemente incrementa il valore memorizzato nella tabella per l'ID specificato (ma lo sai già). Puoi espandermi su come potrei usare ROW_NUMBER()?
Max Vernon,

@MaxVernon non senza sapere cosa LastIDsignifica veramente nel tuo modello. Qual è il suo scopo? Il nome non è esattamente autoesplicativo. Come lo usa Access?
Aaron Bertrand

Una funzione in Access vuole aggiungere una riga a una determinata tabella che non ha IDENTITÀ. First Access chiama GetNextID('WhatevertheIDFieldIsCalled')per ottenere l'ID successivo da utilizzare, quindi lo inserisce nella nuova riga insieme a tutti i dati necessari.
Max Vernon,

Implementerò il tuo cambiamento. Un puro caso di "less is more"!
Max Vernon,

1
Il deadlock fisso potrebbe riemergere. Anche il tuo secondo modello è vulnerabile: sqlblog.com/blogs/alexander_kuznetsov/archive/2010/01/12/… Per eliminare i deadlock userei sp_getapplock. È possibile che il sistema di caricamento misto con centinaia di utenti non abbia deadlock.
AK,
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.