Database
 sql >> Database >  >> RDS >> Database

Il livello di isolamento di lettura non vincolato

[Vedi l'indice per l'intera serie]

La lettura senza commit è il più debole dei quattro livelli di isolamento delle transazioni definiti nello standard SQL (e dei sei implementati in SQL Server). Consente tutti e tre i cosiddetti "fenomeni di concorrenza", letture sporche , letture non ripetibili e fantasmi:

La maggior parte delle persone del database è a conoscenza di questi fenomeni, almeno a grandi linee, ma non tutti si rendono conto che non descrivono completamente le garanzie di isolamento offerte; né descrivono intuitivamente i diversi comportamenti che ci si può aspettare in un'implementazione specifica come SQL Server. Ne parleremo più avanti.

Isolamento della transazione:la 'I' in ACID

Ogni comando SQL viene eseguito all'interno di una transazione (esplicita, implicita o con commit automatico). Ogni transazione ha un livello di isolamento associato, che determina quanto è isolata dagli effetti di altre transazioni simultanee. Questo concetto in qualche modo tecnico ha importanti implicazioni per il modo in cui le query vengono eseguite e la qualità dei risultati che producono.

Considera una semplice query che conta tutte le righe in una tabella. Se questa query potesse essere eseguita istantaneamente (o con zero modifiche simultanee dei dati), potrebbe esserci una sola risposta corretta:il numero di righe fisicamente presenti nella tabella in quel momento. In realtà, l'esecuzione della query richiederà una certa quantità di tempo e il risultato dipenderà dal numero di righe effettivamente incontrate dal motore di esecuzione mentre attraversa qualsiasi struttura fisica scelta per accedere ai dati.

Se le righe vengono aggiunte (o eliminate dalla) tabella da transazioni simultanee mentre è in corso l'operazione di conteggio, è possibile ottenere risultati diversi a seconda che la transazione di conteggio delle righe incontri tutte, alcune o nessuna di queste modifiche simultanee, che a sua volta dipende dal livello di isolamento della transazione di conteggio delle righe.

A seconda del livello di isolamento, dei dettagli fisici e dei tempi delle operazioni simultanee, la nostra transazione di conteggio potrebbe persino produrre un risultato che non è mai stato un vero riflesso dello stato di commit della tabella in qualsiasi momento durante la transazione.

Esempio

Si consideri una transazione di conteggio delle righe che inizia all'ora T1 ed esegue la scansione della tabella dall'inizio alla fine (nell'ordine delle chiavi dell'indice cluster, per motivi di argomento). In quel momento, ci sono 100 righe impegnate nella tabella. Qualche tempo dopo (all'ora T2), la nostra transazione di conteggio ha rilevato 50 di quelle righe. Allo stesso tempo, una transazione simultanea inserisce due righe nella tabella e si impegna poco dopo all'istante T3 (prima che la transazione di conteggio termini). Una delle righe inserite rientra nella metà della struttura dell'indice cluster che la nostra transazione di conteggio ha già elaborato, mentre l'altra riga inserita si trova nella parte non conteggiata.

Al termine della transazione di conteggio delle righe, in questo scenario verranno riportate 101 righe; 100 righe inizialmente nella tabella più la singola riga inserita rilevata durante la scansione. Questo risultato è in contrasto con la cronologia di commit della tabella:c'erano 100 righe di commit ai tempi T1 e T2, quindi 102 righe di commit al momento T3. Non c'è mai stato un momento in cui c'erano 101 righe impegnate.

La cosa sorprendente (forse, a seconda di quanto profondamente hai pensato a queste cose in precedenza) è che questo risultato è possibile al livello di isolamento di lettura commit predefinito (blocco) e anche in isolamento di lettura ripetibile. Entrambi questi livelli di isolamento sono garantiti per leggere solo i dati sottoposti a commit, tuttavia abbiamo ottenuto un risultato che non rappresenta lo stato di commit del database!

Analisi

L'unico livello di isolamento della transazione che fornisce un isolamento completo dagli effetti di concorrenza è serializzabile. L'implementazione di SQL Server del livello di isolamento serializzabile significa che una transazione vedrà i dati sottoposti a commit più recenti, dal momento in cui i dati sono stati bloccati per l'accesso per la prima volta. Inoltre, è garantito che il set di dati rilevato in isolamento serializzabile non modifichi la sua appartenenza prima del termine della transazione.

L'esempio del conteggio delle righe evidenzia un aspetto fondamentale della teoria dei database:dobbiamo essere chiari su cosa significhi un risultato "corretto" per un database che subisce modifiche simultanee e dobbiamo comprendere i compromessi che stiamo facendo quando selezioniamo un isolamento livello inferiore a serializzabile.

Se abbiamo bisogno di una visione point-in-time dello stato di commit del database, dovremmo usare l'isolamento dello snapshot (per le garanzie a livello di transazione) o leggere l'isolamento dello snapshot commit (per le garanzie a livello di istruzione). Si noti tuttavia che una vista point-in-time significa che non stiamo necessariamente operando sull'attuale stato di commit del database; in effetti, potremmo utilizzare informazioni non aggiornate. D'altra parte, se siamo soddisfatti dei risultati basati solo sui dati sottoposti a commit (anche se possibilmente da momenti diversi), potremmo scegliere di attenerci al livello di isolamento predefinito con lock read commit.

Per essere sicuri di produrre risultati (e prendere decisioni!) in base all'ultimo set di dati impegnati, per una cronologia seriale delle operazioni sul database, avremmo bisogno dell'isolamento delle transazioni serializzabile. Ovviamente questa opzione è in genere la più costosa in termini di utilizzo delle risorse e minore concorrenza (incluso un aumento del rischio di deadlock).

Nell'esempio di conteggio delle righe, entrambi i livelli di isolamento dello snapshot (SI e RCSI) darebbero un risultato di 100 righe, che rappresentano il conteggio delle righe impegnate all'inizio dell'istruzione (e della transazione in questo caso). L'esecuzione della query al blocco della lettura con commit o dell'isolamento della lettura ripetibile potrebbe produrre un risultato di 100, 101 o 102 righe, a seconda della tempistica, della granularità del blocco, della posizione di inserimento della riga e del metodo di accesso fisico scelto. In isolamento serializzabile, il risultato sarebbe 100 o 102 righe, a seconda di quale delle due transazioni simultanee si considera eseguita per prima.

Quanto male viene letto senza commit?

Avendo introdotto l'isolamento di lettura senza commit come il più debole dei livelli di isolamento disponibili, dovresti aspettarti che offra garanzie di isolamento ancora inferiori rispetto al blocco di lettura con commit (il livello di isolamento successivo più alto). In effetti lo fa; ma la domanda è:quanto è peggio del blocco dell'isolamento in lettura commessa?

Per iniziare con il contesto corretto, ecco un elenco dei principali effetti di concorrenza che possono essere riscontrati con il livello di isolamento di lettura commit di blocco predefinito di SQL Server:

  • Righe impegnate mancanti
  • Righe incontrate più volte
  • Diverse versioni della stessa riga rilevate in una singola istruzione/piano di query
  • Dati di colonna impegnati da diversi momenti nella stessa riga (esempio)

Questi effetti di concorrenza sono tutti dovuti all'implementazione del blocco di read commit che accetta solo blocchi condivisi a brevissimo termine durante la lettura dei dati. Il livello di isolamento della lettura senza commit fa un ulteriore passo avanti, non accettando affatto i blocchi condivisi, con conseguente possibilità aggiuntiva di "letture sporche".

Letture sporche

Come rapido promemoria, una "lettura sporca" si riferisce alla lettura di dati che vengono modificati da un'altra transazione simultanea (dove "modifica" incorpora operazioni di inserimento, aggiornamento, eliminazione e unione). In altre parole, una lettura sporca si verifica quando una transazione legge i dati che un'altra transazione ha modificato, prima che la transazione di modifica abbia eseguito il commit o l'interruzione di tali modifiche.

Vantaggi e svantaggi

I vantaggi principali dell'isolamento in lettura non vincolata sono il ridotto potenziale di blocco e deadlock a causa di blocchi incompatibili (incluso il blocco non necessario dovuto all'escalation dei blocchi) e possibilmente prestazioni migliorate (evitando la necessità di acquisire e rilasciare blocchi condivisi).

Lo svantaggio potenziale più ovvio dell'isolamento non vincolato di lettura è (come suggerisce il nome) che potremmo leggere dati non vincolati (anche dati che mai commesso, in caso di rollback di una transazione). In un database in cui i rollback sono relativamente rari, la questione della lettura dei dati non vincolati potrebbe essere vista come un semplice problema di tempistica, poiché i dati in questione verranno sicuramente salvati in una fase, e probabilmente abbastanza presto. Abbiamo già riscontrato incongruenze relative alla tempistica nell'esempio del conteggio delle righe (che operava a un livello di isolamento più elevato), quindi ci si potrebbe chiedere quanto sia preoccupante leggere i dati "troppo presto".

Chiaramente la risposta dipende dalle priorità e dal contesto locali, ma sembra certamente possibile una decisione informata di utilizzare l'isolamento non vincolato di lettura. C'è altro a cui pensare però. L'implementazione di SQL Server del livello di isolamento di lettura non vincolata include alcuni comportamenti sottili di cui dobbiamo essere consapevoli prima di effettuare quella "scelta informata".

Scansioni degli ordini di allocazione

L'uso dell'isolamento di lettura non vincolato viene considerato da SQL Server come un segnale che siamo pronti ad accettare le incoerenze che potrebbero sorgere come risultato di una scansione ordinata allocazione.

Di solito, il motore di archiviazione può scegliere una scansione ordinata allocazione solo se i dati sottostanti sono garantiti per non cambiare durante la scansione (perché, ad esempio, il database è di sola lettura o è stato specificato un suggerimento di blocco della tabella). Tuttavia, quando è in uso l'isolamento di lettura senza commit, il motore di archiviazione può comunque scegliere una scansione ordinata allocazione anche se i dati sottostanti potrebbero essere modificati da transazioni simultanee.

In queste circostanze, la scansione in base all'ordine di allocazione può perdere completamente alcuni dati impegnati o incontrare altri dati impegnati più di una volta. L'enfasi è sul conteggio mancante o doppio commesso data (non lettura di dati non vincolati) quindi non è un caso di "letture sporche" in quanto tali. Questa decisione progettuale (per consentire scansioni ordinate allocazione in isolamento di lettura non vincolata) è vista da alcune persone come piuttosto controversa.

Come avvertimento, dovrei essere chiaro che il rischio più generale di perdere o di contare due volte le righe impegnate non si limita a leggere l'isolamento non vincolato. È certamente possibile vedere effetti simili con il blocco della lettura impegnata e ripetibile (come abbiamo visto in precedenza), ma ciò avviene tramite un meccanismo diverso. Righe impegnate mancanti o incontrate più volte a causa di una scansione ordinata allocazione sui dati in modifica è specifico per l'utilizzo dell'isolamento di lettura senza commit.

Lettura di dati "corrotti"

Risultati che sembrano sfidare la logica (e persino controllare i vincoli!) sono possibili con l'isolamento bloccato in lettura commessa (di nuovo, vedere questo articolo di Craig Freedman per alcuni esempi). Per riassumere, il punto è che il blocco della lettura con commit può vedere i dati sottoposti a commit da diversi momenti, anche per una singola riga se, ad esempio, il piano di query utilizza tecniche come l'intersezione degli indici.

Questi risultati possono essere imprevisti, ma sono completamente in linea con la garanzia di leggere solo i dati impegnati. Non si può sfuggire al fatto che garanzie di coerenza dei dati più elevate richiedono livelli di isolamento più elevati.

Questi esempi potrebbero anche essere piuttosto scioccanti, se non li hai mai visti prima. Gli stessi risultati sono possibili con l'isolamento di lettura non vincolata, ovviamente, ma consentire letture sporche aggiunge una dimensione in più:i risultati possono includere dati impegnati e non vincolati da momenti diversi, anche per la stessa riga.

Andando oltre, è anche possibile che una transazione di lettura non vincolata legga un valore di una singola colonna in uno stato misto di dati impegnati e non vincolati. Ciò può verificarsi durante la lettura di un valore LOB (ad esempio, xml o uno qualsiasi dei tipi "max") se il valore è archiviato su più pagine di dati. Una lettura non vincolata può incontrare dati impegnati o non vincolati da diversi momenti su pagine diverse, risultando in un valore finale a colonna singola che è una combinazione di valori!

Per fare un esempio, considera una singola colonna varchar(max) che inizialmente contiene 10.000 caratteri 'x'. Una transazione simultanea aggiorna questo valore a 10.000 caratteri 'y'. Una transazione di lettura non vincolata può leggere i caratteri "x" da una pagina della LOB e i caratteri "y" da un'altra, risultando in un valore di lettura finale contenente una combinazione di caratteri "x" e "y". È difficile sostenere che questo non rappresenti la lettura di dati "corrotti".

Dimostrazione

Crea una tabella in cluster con una singola riga di dati LOB:

CREATE TABLE dbo.Test
(
    RowID integer PRIMARY KEY,
    LOB varchar(max) NOT NULL,
);
 
INSERT dbo.Test
    (RowID, LOB)
VALUES
    (1, REPLICATE(CONVERT(varchar(max), 'X'), 16100));

In una sessione separata, esegui lo script seguente per leggere il valore LOB in lettura isolamento non vincolato:

-- Run this in session 2
SET NOCOUNT ON;
 
DECLARE 
    @ValueRead varchar(max) = '',
    @AllXs varchar(max) = REPLICATE(CONVERT(varchar(max), 'X'), 16100),
    @AllYs varchar(max) = REPLICATE(CONVERT(varchar(max), 'Y'), 16100);
 
WHILE 1 = 1
BEGIN
    SELECT @ValueRead = T.LOB
    FROM dbo.Test AS T WITH (READUNCOMMITTED)
    WHERE T.RowID = 1;
 
    IF @ValueRead NOT IN (@AllXs, @AllYs)
    BEGIN
    	PRINT LEFT(@ValueRead, 8000);
        PRINT RIGHT(@ValueRead, 8000);
        BREAK;
    END
END;

Nella prima sessione, esegui questo script per scrivere valori alternati nella colonna LOB:

-- Run this in session 1
SET NOCOUNT ON;
 
DECLARE 
    @AllXs varchar(max) = REPLICATE(CONVERT(varchar(max), 'X'), 16100),
    @AllYs varchar(max) = REPLICATE(CONVERT(varchar(max), 'Y'), 16100);
 
WHILE 1 = 1
BEGIN
    UPDATE dbo.Test
    SET LOB = @AllYs
    WHERE RowID = 1;
 
    UPDATE dbo.Test
    SET LOB = @AllXs
    WHERE RowID = 1;
END;

Dopo poco tempo, lo script nella seconda sessione terminerà, dopo aver letto uno stato misto per il valore LOB, ad esempio:

Questo particolare problema è limitato alle letture dei valori delle colonne LOB distribuiti su più pagine, non a causa delle garanzie fornite dal livello di isolamento, ma perché SQL Server utilizza i latch a livello di pagina per garantire l'integrità fisica. Un effetto collaterale di questo dettaglio di implementazione è che impedisce tali letture di dati "corrotti" se i dati per una singola operazione di lettura risiedono su una singola pagina.

A seconda della versione di SQL Server in uso, se i dati di "stato misto" vengono letti per una colonna xml, si otterrà un errore risultante dal risultato xml possibilmente non corretto, nessun errore o l'errore specifico non sottoposto a commit 601 , "impossibile continuare la scansione con NOLOCK a causa dello spostamento dei dati." La lettura di dati a stato misto per altri tipi di LOB non genera generalmente un messaggio di errore; l'applicazione o la query che consuma non ha modo di sapere che ha appena subito il peggior tipo di lettura sporca. Per completare l'analisi, una riga a stato misto non LOB letta come risultato di un'intersezione di indice non viene mai segnalata come errore.

Il messaggio qui è che se usi l'isolamento di lettura senza commit, accetti che le letture sporche includano la possibilità di leggere valori LOB a stato misto "corrotti".

Il suggerimento NOLOCK

Suppongo che nessuna discussione sul livello di isolamento non vincolato di lettura sarebbe completa senza almeno menzionare questo suggerimento sulla tabella (ampiamente abusato e frainteso). Il suggerimento stesso è solo un sinonimo del suggerimento tabella READUNCOMMITTED. Svolge esattamente la stessa funzione:si accede all'oggetto a cui è applicato utilizzando la semantica di isolamento di lettura non vincolata (sebbene vi sia un'eccezione).

Per quanto riguarda il nome "NOLOCK", significa semplicemente che non vengono presi lock condivisi durante la lettura dei dati . Altri blocchi (stabilità dello schema, blocchi esclusivi per la modifica dei dati e così via) vengono comunque presi normalmente.

In generale, i suggerimenti NOLOCK dovrebbero essere comuni quanto altri suggerimenti della tabella del livello di isolamento per oggetto come SERIALIZABLE e READCOMMITTEDLOCK. Vale a dire:non molto comune e usato solo dove non c'è una buona alternativa, uno scopo ben definito e un completo comprensione delle conseguenze.

Un esempio di uso legittimo di NOLOCK (o READUNCOMMITTED) è quando si accede a DMV o altre viste di sistema, dove un livello di isolamento più elevato potrebbe causare contese indesiderate su strutture di dati non utente. Un altro esempio di caso limite potrebbe essere quando una query deve accedere a una porzione significativa di una tabella di grandi dimensioni, che garantisce che non subiranno mai modifiche ai dati mentre la query suggerita è in esecuzione. Ci dovrebbe essere una buona ragione per non utilizzare lo snapshot o leggere invece l'isolamento dello snapshot commit e gli aumenti delle prestazioni previsti dovrebbero essere testati, convalidati e confrontati, ad esempio, con un unico suggerimento di blocco della tabella condivisa.

L'uso meno desiderabile di NOLOCK è quello purtroppo più comune:applicarlo a ogni oggetto in una query come una sorta di interruttore magico per accelerare. Con la migliore volontà del mondo, non esiste un modo migliore per far sembrare il codice di SQL Server decisamente amatoriale. Se hai legittimamente bisogno di leggere l'isolamento non vincolato per una query, un blocco di codice o un modulo, è probabilmente meglio impostare il livello di isolamento della sessione in modo appropriato e fornire commenti per giustificare l'azione.

Pensieri finali

La lettura non vincolata è una scelta legittima per il livello di isolamento delle transazioni, ma deve essere una scelta informata. A titolo di promemoria, di seguito sono riportati alcuni dei fenomeni di concorrenza possibili nell'isolamento di lettura commit del blocco predefinito di SQL Server:

  • Righe precedentemente impegnate mancanti
  • Righe impegnate incontrate più volte
  • Diverse versioni salvate della stessa riga rilevate in una singola istruzione/piano di query
  • Dati impegnati da diversi momenti nella stessa riga (ma colonne diverse)
  • Letture di dati impegnati che sembrano contraddire i vincoli abilitati e controllati

A seconda del tuo punto di vista, potrebbe essere un elenco piuttosto scioccante di possibili incongruenze per il livello di isolamento predefinito. A quell'elenco, leggi l'isolamento non vincolato aggiunge:

  • Letture sporche (rilevamento di dati che non sono stati ancora sottoposti a commit e potrebbero non esserlo mai)
  • Righe contenenti una combinazione di dati impegnati e non vincolati
  • Righe perse/duplicate a causa di scansioni ordinate allocazione
  • Valori LOB individuali (a colonna singola) a stato misto ("corrotti").
  • Errore 601 – "Impossibile continuare la scansione con NOLOCK a causa dello spostamento dei dati" (esempio).

Se le tue principali preoccupazioni transazionali riguardano gli effetti collaterali del blocco dell'isolamento con commit in lettura (blocco, sovraccarico di blocco, concorrenza ridotta a causa dell'escalation del blocco e così via), potresti essere meglio servito da un livello di isolamento del controllo delle versioni delle righe come l'isolamento dello snapshot con commit di lettura (RCSI) o isolamento snapshot (SI). Questi non sono tuttavia gratuiti e in particolare gli aggiornamenti sotto RCSI hanno alcuni comportamenti controintuitivi.

Per gli scenari che richiedono i massimi livelli di garanzie di coerenza, serializzabile rimane l'unica scelta sicura. Per le operazioni critiche per le prestazioni sui dati di sola lettura (ad esempio, database di grandi dimensioni che sono effettivamente di sola lettura tra finestre ETL), anche l'impostazione esplicita del database su READ_ONLY può essere una buona scelta (i blocchi condivisi non vengono presi quando il database è di sola lettura e non vi è alcun rischio di incoerenza).

Ci sarà anche un numero relativamente piccolo di applicazioni per le quali la lettura dell'isolamento non vincolato è la scelta giusta. Queste applicazioni devono essere soddisfatte con risultati approssimativi e la possibilità di dati occasionalmente incoerenti, apparentemente non validi (in termini di vincoli) o "probabilmente corrotti". Se i dati cambiano relativamente di rado, anche il rischio di queste incoerenze è corrispondentemente inferiore.

[Vedi l'indice per l'intera serie]