Domanda

Stiamo riscontrando alcune condizioni di stallo dannose, ma rare, nel database Stack Overflow SQL Server 2005.

Ho allegato il profiler, ho impostato un profilo di traccia utilizzando questo eccellente articolo sulla risoluzione dei problemi di stallo, e ho catturato una serie di esempi.La cosa strana è questa la scrittura deadlock è Sempre lo stesso:

UPDATE [dbo].[Posts]
SET [AnswerCount] = @p1, [LastActivityDate] = @p2, [LastActivityUserId] = @p3
WHERE [Id] = @p0

L'altra affermazione di deadlock varia, ma di solito è banale e semplice Leggere della tabella dei post.Questo viene sempre ucciso nello stallo.Ecco un esempio

SELECT
[t0].[Id], [t0].[PostTypeId], [t0].[Score], [t0].[Views], [t0].[AnswerCount], 
[t0].[AcceptedAnswerId], [t0].[IsLocked], [t0].[IsLockedEdit], [t0].[ParentId], 
[t0].[CurrentRevisionId], [t0].[FirstRevisionId], [t0].[LockedReason],
[t0].[LastActivityDate], [t0].[LastActivityUserId]
FROM [dbo].[Posts] AS [t0]
WHERE [t0].[ParentId] = @p0

Per essere perfettamente chiari, non stiamo vedendo deadlock di scrittura/scrittura, ma di lettura/scrittura.

Al momento disponiamo di un misto di query LINQ e SQL parametrizzate.Abbiamo aggiunto with (nolock) a tutte le query SQL.Questo potrebbe aver aiutato alcuni.Abbiamo anche avuto una singola query sul badge (molto) scritta male che ho corretto ieri, che impiegava più di 20 secondi per essere eseguita ogni volta, e in più veniva eseguita ogni minuto.Speravo che questa fosse la fonte di alcuni dei problemi di bloccaggio!

Sfortunatamente, ho ricevuto un altro errore di stallo circa 2 ore fa.Stessi identici sintomi, stesso esatto colpevole scritto.

La cosa veramente strana è che l'istruzione SQL di scrittura bloccata che vedi sopra fa parte di un percorso di codice molto specifico.Suo soltanto eseguito quando viene aggiunta una nuova risposta a una domanda: aggiorna la domanda principale con il nuovo conteggio delle risposte e l'ultima data/utente.Questo, ovviamente, non è così comune rispetto all’enorme numero di letture che stiamo facendo!Per quanto ne so, non stiamo eseguendo un numero enorme di scritture in nessuna parte dell'app.

Mi rendo conto che NOLOCK è una specie di martello gigante, ma la maggior parte delle query che eseguiamo qui non devono essere così precise.Ti interesserà se il tuo profilo utente è obsoleto di qualche secondo?

Usare NOLOCK con Linq è un po' più difficile in quanto Scott Hanselman ne parla qui.

Stiamo flirtando con l'idea di usare

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED

sul contesto del database di base in modo che tutte le nostre query LINQ abbiano questo set.Senza ciò, dovremmo racchiudere ogni chiamata LINQ che facciamo (beh, quelle di semplice lettura, che sono la stragrande maggioranza) in un blocco di codice di transazione di 3-4 righe, il che è brutto.

Immagino di essere un po 'frustrato dal fatto che letture banali in SQL 2005 possano bloccarsi nelle scritture.Potevo vedere che i deadlock di scrittura/scrittura fossero un grosso problema, ma legge? Non gestiamo un sito bancario qui, non abbiamo bisogno di una precisione perfetta ogni volta.

Idee?Pensieri?


Stai istanziando un nuovo oggetto LINQ to SQL DataContext per ogni operazione o forse condividi lo stesso contesto statico per tutte le tue chiamate?

Jeremy, stiamo condividendo per la maggior parte un contesto dati statico nel controller di base:

private DBContext _db;
/// <summary>
/// Gets the DataContext to be used by a Request's controllers.
/// </summary>
public DBContext DB
{
    get
    {
        if (_db == null)
        {
            _db = new DBContext() { SessionName = GetType().Name };
            //_db.ExecuteCommand("SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED");
        }
        return _db;
    }
}

Consigliate di creare un nuovo contesto per ogni Controller, o per Pagina, o...più spesso?

È stato utile?

Soluzione

Secondo MSDN:

http://msdn.microsoft.com/en-us/library/ms191242.aspx

Quando le opzioni di database di isolamento dell'istantanea sono state attivate le opzioni di database di isolamento di lettura, vengono mantenute copie logiche (versioni) per tutte le modifiche dei dati eseguite nel database.Ogni volta che una riga viene modificata da una transazione specifica, l'istanza del motore del database memorizza una versione dell'immagine precedentemente impegnata della riga in tempdb.Ogni versione è contrassegnata con il numero di sequenza di transazione della transazione che ha apportato la modifica.Le versioni delle righe modificate sono incatenate utilizzando un elenco di collegamenti.Il nuovo valore di riga è sempre archiviato nel database corrente e incatenato alle righe in versione memorizzate in tempdb.

Per le transazioni a breve termine, una versione di una riga modificata può essere memorizzata nella cache nel pool di buffer senza essere scritta nei file disco del database TempDB.Se la necessità della riga in versione è di breve durata, verrà semplicemente lasciata cadere dal pool di buffer e potrebbe non necessariamente incorrere in sovraccarico di I/O.

Sembra esserci una leggera penalità in termini di prestazioni per le spese generali aggiuntive, ma potrebbe essere trascurabile.Dovremmo fare dei test per esserne sicuri.

Prova a impostare questa opzione e RIMUOVERE tutti i NOLOCK dalle query di codice a meno che non sia realmente necessario.I NOLOCK o l'utilizzo di metodi globali nel gestore del contesto del database per combattere i livelli di isolamento delle transazioni del database sono soluzioni al problema.NOLOCKS maschererà i problemi fondamentali con il nostro livello dati e probabilmente porterà alla selezione di dati inaffidabili, dove il controllo automatico delle versioni delle righe di selezione/aggiornamento sembra essere la soluzione.

ALTER Database [StackOverflow.Beta] SET READ_COMMITTED_SNAPSHOT ON

Altri suggerimenti

NOLOCK E LEGGI SENZA IMPEGNO sono un pendio scivoloso.Non dovresti mai usarli se non capisci prima perché si sta verificando lo stallo.Mi preoccuperebbe che tu dicessi: "Abbiamo aggiunto con (nolock) a tutte le query SQL".È necessario aggiungere CON NOLOCK ovunque è un segno sicuro che hai problemi nel tuo livello dati.

La stessa dichiarazione di aggiornamento sembra un po' problematica.Determinate il conteggio in precedenza nella transazione o lo estraete semplicemente da un oggetto? AnswerCount = AnswerCount+1 quando viene aggiunta una domanda è probabilmente un modo migliore per gestirla.Quindi non hai bisogno di una transazione per ottenere il conteggio corretto e non devi preoccuparti del problema di concorrenza a cui ti stai potenzialmente esponendo.

Un modo semplice per aggirare questo tipo di problema di stallo senza molto lavoro e senza abilitare letture sporche è utilizzare "Snapshot Isolation Mode" (nuovo in SQL 2005) che ti darà sempre una lettura pulita degli ultimi dati non modificati.Puoi anche catturare e riprovare le istruzioni bloccate abbastanza facilmente se vuoi gestirle con garbo.

La domanda dell'OP era chiedere perché si è verificato questo problema.Questo post spera di rispondere lasciando che le possibili soluzioni siano elaborate da altri.

Probabilmente si tratta di un problema correlato all'indice.Ad esempio, supponiamo che la tabella Post abbia un indice X non cluster che contiene il ParentID e uno (o più) dei campi da aggiornare (AnswerCount, LastActivityDate, LastActivityUserId).

Si verificherebbe un deadlock se il cmd SELECT esegue un blocco di lettura condivisa sull'indice X per eseguire la ricerca in base al ParentId e quindi deve eseguire un blocco di lettura condivisa sull'indice cluster per ottenere le colonne rimanenti mentre il cmd UPDATE esegue un blocco di scrittura esclusivo bloccare l'indice cluster ed è necessario ottenere un blocco esclusivo di scrittura sull'indice X per aggiornarlo.

Ora ti trovi in ​​una situazione in cui A ha bloccato X e sta cercando di ottenere Y mentre B ha bloccato Y e sta cercando di ottenere X.

Naturalmente, avremo bisogno che l'OP aggiorni il suo post con maggiori informazioni su quali indici sono in gioco per confermare se questa è effettivamente la causa.

Sono piuttosto a disagio per questa domanda e le risposte dell'addetto.Ci sono molti "prova questa polvere magica!"No, quella polvere magica!"

Non riesco a vedere da nessuna parte che tu abbia analizzato i blocchi che sono stati presi e determinato quale tipo esatto di blocchi sono bloccati.

Tutto quello che hai indicato è che si verificano alcuni blocchi, non ciò che è un deadlock.

In SQL 2005 puoi ottenere maggiori informazioni su quali blocchi vengono rimossi utilizzando:

DBCC TRACEON (1222, -1)

in modo che quando si verifica lo stallo avrai una diagnostica migliore.

Stai istanziando un nuovo oggetto LINQ to SQL DataContext per ogni operazione o forse condividi lo stesso contesto statico per tutte le tue chiamate?Inizialmente avevo provato quest'ultimo approccio e, da quello che ricordo, causava un blocco indesiderato nel DB.Ora creo un nuovo contesto per ogni operazione atomica.

Prima di bruciare la casa per catturare una mosca con NOLOCK dappertutto, potresti dare un'occhiata a quel grafico di stallo che avresti dovuto catturare con Profiler.

Ricorda che un deadlock richiede (almeno) 2 lock.La Connessione 1 ha il Blocco A, vuole il Blocco B e viceversa per la Connessione 2.Questa è una situazione irrisolvibile e qualcuno deve dare.

Ciò che hai mostrato finora viene risolto con un semplice blocco, cosa che Sql Server è felice di fare tutto il giorno.

Sospetto che tu (o LINQ) stiate avviando una transazione con l'istruzione UPDATE al suo interno e selezionando in anticipo qualche altra informazione.Ma devi davvero tornare indietro nel grafico dei deadlock per trovare i blocchi tenuto da ciascun thread, quindi tornare indietro tramite Profiler per trovare le istruzioni che hanno causato la concessione di tali blocchi.

Mi aspetto che ci siano almeno 4 istruzioni per completare questo puzzle (o un'istruzione che accetta più blocchi - forse c'è un trigger nella tabella Post?).

Ti interesserà se il tuo profilo utente è obsoleto di qualche secondo?

No, è perfettamente accettabile.L'impostazione del livello di isolamento delle transazioni di base è probabilmente il modo migliore/più pulito da percorrere.

Il tipico deadlock di lettura/scrittura deriva dall'accesso all'ordine dell'indice.Read (T1) individua la riga sull'indice A e quindi cerca la colonna proiettata sull'indice B (solitamente raggruppata).Write (T2) cambia l'indice B (il cluster) quindi deve aggiornare l'indice A.T1 ha S-Lck su A, vuole S-Lck su B, T2 ha X-Lck su B, vuole U-Lck su A.Vicolo cieco, sbuffo.T1 viene ucciso.Questo è prevalente in ambienti con traffico OLTP intenso e solo un po' troppi indici :).La soluzione è fare in modo che la lettura non debba saltare da A a B (es.inclusa la colonna in A o rimuovi la colonna dall'elenco proiettato) oppure T2 non deve passare da B ad A (non aggiornare la colonna indicizzata).Sfortunatamente, linq non è tuo amico qui...

@Jeff - Sicuramente non sono un esperto in materia, ma ho ottenuto buoni risultati istanziando un nuovo contesto su quasi ogni chiamata.Penso che sia simile alla creazione di un nuovo oggetto Connection su ogni chiamata con ADO.Il sovraccarico non è così grave come si potrebbe pensare, poiché il pool di connessioni verrà comunque utilizzato.

Utilizzo semplicemente un helper statico globale come questo:

public static class AppData
{
    /// <summary>
    /// Gets a new database context
    /// </summary>
    public static CoreDataContext DB
    {
        get
        {
            var dataContext = new CoreDataContext
            {
                DeferredLoadingEnabled = true
            };
            return dataContext;
        }
    }
}

e poi faccio qualcosa del genere:

var db = AppData.DB;

var results = from p in db.Posts where p.ID = id select p;

E farei la stessa cosa per gli aggiornamenti.Ad ogni modo, non ho tanto traffico quanto te, ma stavo sicuramente ottenendo un certo blocco quando ho utilizzato un DataContext condiviso all'inizio con solo una manciata di utenti.Nessuna garanzia, ma potrebbe valere la pena provarlo.

Aggiornamento:Poi di nuovo, guardando il tuo codice, stai condividendo il contesto dei dati solo per la durata di quella particolare istanza del controller, il che sostanzialmente sembra corretto a meno che non venga in qualche modo utilizzato contemporaneamente da più chiamate all'interno del controller.In un thread sull'argomento, ScottGu ha detto:

I controllori vivono solo per una singola richiesta, quindi alla fine dell'elaborazione di una richiesta vengono raccolti i rifiuti (il che significa che viene raccolto il DataContext)...

Ad ogni modo, potrebbe non essere così, ma probabilmente vale la pena provare, magari insieme ad alcuni test di carico.

Q.Perché stai conservando il file AnswerCount nel Posts tavolo in primo luogo?

Un approccio alternativo consiste nell'eliminare il "write back" nel file Posts tabella non memorizzando il file AnswerCount nella tabella ma per calcolare dinamicamente il numero di risposte al post come richiesto.

Sì, ciò significherà che stai eseguendo una query aggiuntiva:

SELECT COUNT(*) FROM Answers WHERE post_id = @id

o più tipicamente (se lo visualizzi per la home page):

SELECT p.post_id, 
     p.<additional post fields>,
     a.AnswerCount
FROM Posts p
    INNER JOIN AnswersCount_view a
    ON <join criteria>
WHERE <home page criteria>

ma questo in genere si traduce in un INDEX SCAN e può essere più efficiente nell’uso delle risorse rispetto all’utilizzo READ ISOLATION.

C'è più di un modo per scuoiare un gatto.La denormalizzazione prematura di uno schema di database può introdurre problemi di scalabilità.

Vuoi assolutamente che READ_COMMITTED_SNAPSHOT sia attivato, cosa che non è predefinita.Questo ti dà la semantica MVCC.È la stessa cosa che Oracle usa per impostazione predefinita.Avere un database MVCC è incredibilmente utile, NON usarne uno è folle.Ciò ti consente di eseguire quanto segue all'interno di una transazione:

Aggiorna UTENTI Set FirstName = 'foobar';//decido di dormire per un anno.

nel frattempo, senza impegnarsi in quanto sopra, tutti possono continuare a scegliere tranquillamente da quella tabella.Se non hai familiarità con MVCC, rimarrai scioccato dal fatto di poter vivere senza di esso.Sul serio.

Impostare l'impostazione predefinita su lettura senza impegno non è una buona idea.Indubbiamente introdurrete delle incoerenze e vi ritroverete con un problema peggiore di quello che avete adesso.L'isolamento degli snapshot potrebbe funzionare bene, ma rappresenta un cambiamento drastico nel modo in cui funziona SQL Server e inserisce un file Enorme caricare su tempdb.

Ecco cosa dovresti fare:utilizzare try-catch (in T-SQL) per rilevare la condizione di deadlock.Quando ciò accade, è sufficiente eseguire nuovamente la query.Questa è la pratica standard di programmazione del database.

Ci sono buoni esempi di questa tecnica in Paul Nielson Bibbia di SQL Server 2005.

Ecco un modello rapido che utilizzo:

-- Deadlock retry template

declare @lastError int;
declare @numErrors int;

set @numErrors = 0;

LockTimeoutRetry:

begin try;

-- The query goes here

return; -- this is the normal end of the procedure

end try begin catch
    set @lastError=@@error
    if @lastError = 1222 or @lastError = 1205 -- Lock timeout or deadlock
    begin;
        if @numErrors >= 3 -- We hit the retry limit
        begin;
            raiserror('Could not get a lock after 3 attempts', 16, 1);
            return -100;
        end;

        -- Wait and then try the transaction again
        waitfor delay '00:00:00.25';
        set @numErrors = @numErrors + 1;
        goto LockTimeoutRetry;

    end;

    -- Some other error occurred
    declare @errorMessage nvarchar(4000), @errorSeverity int
    select    @errorMessage = error_message(),
            @errorSeverity = error_severity()

    raiserror(@errorMessage, @errorSeverity, 1)

    return -100
end catch;    

Una cosa che ha funzionato per me in passato è stata assicurarmi che tutte le mie query e gli aggiornamenti accedano alle risorse (tabelle) nello stesso ordine.

Cioè, se una query viene aggiornata nell'ordine Tabella1, Tabella2 e un'altra query la aggiorna nell'ordine Tabella2, Tabella1, potresti visualizzare dei deadlock.

Non sono sicuro che sia possibile modificare l'ordine degli aggiornamenti poiché stai utilizzando LINQ.Ma è qualcosa da guardare.

Ti interesserà se il tuo profilo utente è obsoleto di qualche secondo?

Pochi secondi sarebbero sicuramente accettabili.In ogni caso, non sembra che ci vorrà così tanto tempo, a meno che un numero enorme di persone non invii risposte contemporaneamente.

Sono d'accordo con Jeremy su questo.Chiedi se dovresti creare un nuovo contesto dati per ciascun controller o per pagina: tendo a crearne uno nuovo per ogni query indipendente.

Al momento sto costruendo una soluzione che implementava il contesto statico come fai tu, e quando ho lanciato tonnellate di richieste alla bestia di un server (milione+) durante gli stress test, ricevevo anche blocchi di lettura/scrittura in modo casuale.

Non appena ho cambiato la mia strategia per utilizzare un contesto dati diverso a livello LINQ per query e ho confidato che SQL Server potesse eseguire la sua magia di pooling delle connessioni, i blocchi sembravano scomparire.

Ovviamente avevo una certa pressione in termini di tempo, quindi ho provato una serie di cose contemporaneamente, quindi non posso essere sicuro al 100% che sia quello che ha risolto il problema, ma ho un alto livello di fiducia - mettiamola in questo modo .

Dovresti implementare letture sporche.

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED

Se non richiedi assolutamente la perfetta integrità transazionale con le tue query, dovresti utilizzare letture sporche quando accedi a tabelle con elevata concorrenza.Presumo che la tua tabella Post sia una di quelle.

Questo potrebbe darti le cosiddette "letture fantasma", ovvero quando la tua query agisce sui dati di una transazione che non è stata impegnata.

Non gestiamo un sito bancario qui, non abbiamo bisogno di una precisione perfetta ogni volta

Usa letture sporche.Hai ragione nel dire che non ti daranno una precisione perfetta, ma dovrebbero risolvere i tuoi problemi di blocco morto.

Senza ciò, dovremmo racchiudere ogni chiamata LINQ che facciamo (beh, quelle di semplice lettura, che sono la stragrande maggioranza) in un blocco di codice di transazione di 3-4 righe, il che è brutto

Se implementi letture sporche sul "contesto del database di base", puoi sempre racchiudere le tue chiamate individuali utilizzando un livello di isolamento più elevato se hai bisogno dell'integrità transazionale.

Allora qual è il problema con l'implementazione di un meccanismo di ripetizione?Ci sarà sempre la possibilità che si verifichi una situazione di stallo, quindi perché non avere una logica per identificarlo e riprovare?

Almeno alcune delle altre opzioni non introdurranno penalità di prestazione che vengono applicate continuamente quando un sistema di tentativi si attiva raramente?

Inoltre, non dimenticare una sorta di registrazione quando si verifica un nuovo tentativo, in modo da non ritrovarti in quella situazione in cui raramente diventi spesso.

Ora che vedo la risposta di Jeremy, penso di ricordare di aver sentito che la procedura migliore è utilizzare un nuovo DataContext per ogni operazione sui dati.Rob Conery ha scritto diversi post su DataContext e li aggiorna sempre anziché utilizzare un singleton.

Ecco il modello che abbiamo utilizzato per Video.Show (collegamento alla vista sorgente in CodePlex):

using System.Configuration;
namespace VideoShow.Data
{
  public class DataContextFactory
  {
    public static VideoShowDataContext DataContext()
    {
        return new VideoShowDataContext(ConfigurationManager.ConnectionStrings["VideoShowConnectionString"].ConnectionString);
    }
    public static VideoShowDataContext DataContext(string connectionString)
    {
        return new VideoShowDataContext(connectionString);
    }
  }
}

Quindi a livello di servizio (o ancora più granulare, per gli aggiornamenti):

private VideoShowDataContext dataContext = DataContextFactory.DataContext();

public VideoSearchResult GetVideos(int pageSize, int pageNumber, string sortType)
{
  var videos =
  from video in DataContext.Videos
  where video.StatusId == (int)VideoServices.VideoStatus.Complete
  orderby video.DatePublished descending
  select video;
  return GetSearchResult(videos, pageSize, pageNumber);
}

Dovrei essere d'accordo con Greg purché l'impostazione del livello di isolamento su lettura senza impegno non abbia effetti negativi su altre query.

Sarei interessato a sapere, Jeff, in che modo l'impostazione a livello di database influirebbe su una query come la seguente:

Begin Tran
Insert into Table (Columns) Values (Values)
Select Max(ID) From Table
Commit Tran

A me va bene se il mio profilo è obsoleto anche di diversi minuti.

Stai riprovando la lettura dopo che non è riuscita?È certamente possibile quando si eseguono un sacco di letture casuali che alcuni colpiscano quando non riescono a leggere.La maggior parte delle applicazioni con cui lavoro sono pochissime scritture rispetto al numero di letture e sono sicuro che le letture non sono neanche lontanamente vicine al numero che stai ottenendo.

Se l'implementazione di "READ UNCOMMITTED" non risolve il tuo problema, è difficile aiutarti senza sapere molto di più sull'elaborazione.Potrebbero esserci altre opzioni di ottimizzazione che potrebbero aiutare questo comportamento.A meno che qualche guru di MSSQL non venga in soccorso, consiglio di sottoporre il problema al fornitore.

Continuerei ad accordare tutto;come sta funzionando il sottosistema del disco?Qual è la lunghezza media della coda del disco?Se è in corso il backup degli I/O, il vero problema potrebbe non essere queste due query che creano un deadlock, ma potrebbe essere un'altra query che sta bloccando il sistema;hai menzionato una query che richiede 20 secondi che è stata ottimizzata, ce ne sono altre?

Concentrati sulla riduzione delle query di lunga durata, scommetto che i problemi di stallo scompariranno.

Si è verificato lo stesso problema e non è possibile utilizzare "IsolationLevel = IsolationLevel.ReadUncomtched" su TransactionScope perché il server non ha DTS abilitato (!).

Questo è quello che ho fatto con un metodo di estensione:

public static void SetNoLock(this MyDataContext myDS)
{
    myDS.ExecuteCommand("SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED");
}

Quindi, per coloro che utilizzano tabelle di concorrenza critiche, abilitiamo il "nolock" in questo modo:

using (MyDataContext myDS = new MyDataContext())
{
   myDS.SetNoLock();

   //  var query = from ...my dirty querys here...
}

I suggerimenti sono benvenuti!

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top