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

Modifiche dei dati in isolamento dello snapshot in lettura

[Vedi l'indice per l'intera serie]

Il post precedente di questa serie ha mostrato come un'istruzione T-SQL eseguita con isolamento dello snapshot con commit di lettura (RCSI ) normalmente vede una vista istantanea dello stato di commit del database com'era quando l'istruzione ha iniziato l'esecuzione. Questa è una buona descrizione di come funzionano le cose per le istruzioni che leggono i dati, ma ci sono differenze importanti per istruzioni eseguite in RCSI che modifica le righe esistenti .

Sottolineo la modifica di righe esistenti sopra, perché le seguenti considerazioni si applicano solo a UPDATE e DELETE operazioni (e le azioni corrispondenti di un MERGE dichiarazione). Per essere chiari, INSERT le dichiarazioni sono specificamente escluse dal comportamento che sto per descrivere perché gli inserti non modificano l'esistente dati.

Aggiorna i blocchi e le versioni delle righe

La prima differenza è che le istruzioni di aggiornamento ed eliminazione non leggono le versioni di riga in RCSI durante la ricerca delle righe di origine da modificare. Aggiorna ed elimina le istruzioni in RCSI acquisiscono invece blocchi di aggiornamento durante la ricerca di righe qualificanti. L'utilizzo dei blocchi di aggiornamento garantisce che l'operazione di ricerca trovi le righe da modificare utilizzando i dati confermati più recenti .

Senza i blocchi di aggiornamento, la ricerca sarebbe basata su una versione eventualmente non aggiornata del set di dati (i dati impegnati erano quando è iniziata la dichiarazione di modifica dei dati). Questo potrebbe ricordarti l'esempio di trigger che abbiamo visto l'ultima volta, in cui un READCOMMITTEDLOCK hint è stato utilizzato per ripristinare da RCSI l'implementazione di blocco dell'isolamento con commit di lettura. Quel suggerimento era richiesto in quell'esempio per evitare di basare un'azione importante su informazioni non aggiornate. Lo stesso tipo di ragionamento viene utilizzato qui. Una differenza è che il READCOMMITTEDLOCK hint acquisisce i blocchi condivisi invece dei blocchi di aggiornamento. Inoltre, SQL Server acquisisce automaticamente i blocchi di aggiornamento per proteggere le modifiche ai dati in RCSI senza richiedere l'aggiunta di un suggerimento esplicito.

L'adozione dei blocchi di aggiornamento garantisce inoltre che l'istruzione di aggiornamento o eliminazione venga bloccata se incontra un blocco incompatibile, ad esempio un blocco esclusivo che protegge una modifica dei dati in volo eseguita da un'altra transazione simultanea.

Un'ulteriore complicazione è che il comportamento modificato si applica solo al tavolo che è il obiettivo dell'operazione di aggiornamento o cancellazione. Altre tabelle nello stesso eliminare o aggiornare la dichiarazione, inclusi riferimenti aggiuntivi alla tabella di destinazione, continua a utilizzare le versioni di riga .

Probabilmente sono necessari alcuni esempi per rendere un po' più chiari questi comportamenti confusi…

Impostazione di prova

Il seguente script assicura che siamo tutti impostati per utilizzare RCSI, crea una tabella semplice e aggiunge due righe di esempio:

ALTER DATABASE Sandpit
SET READ_COMMITTED_SNAPSHOT ON
WITH ROLLBACK IMMEDIATE;
GO
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
GO
CREATE TABLE dbo.Test
(
    RowID integer PRIMARY KEY,
    Data integer NOT NULL
);
GO
INSERT dbo.Test
    (RowID, Data)
VALUES 
    (1, 1234),
    (2, 2345);

Il passaggio successivo deve essere eseguito in una sessione separata . Avvia una transazione ed elimina entrambe le righe dalla tabella di test (sembra strano, ma tutto questo avrà un senso a breve):

BEGIN TRANSACTION;
DELETE dbo.Test 
WHERE RowID IN (1, 2);

Tieni presente che la transazione è stata deliberatamente lasciata aperta . Ciò mantiene i blocchi esclusivi su entrambe le righe che vengono eliminate (insieme ai soliti blocchi esclusivi per intento sulla pagina contenitore e sulla tabella stessa) come la query seguente può essere utilizzata per mostrare:

SELECT
    resource_type,
    resource_description,
    resource_associated_entity_id,
    request_mode,
    request_status
FROM sys.dm_tran_locks
WHERE 
    request_session_id = @@SPID;

Il test di selezione

Tornando alla sessione originale , la prima cosa che voglio mostrare è che le normali istruzioni select che utilizzano RCSI vedono ancora le due righe eliminate. La query di selezione seguente utilizza le versioni di riga per restituire gli ultimi dati vincolati al momento dell'inizio dell'istruzione:

SELECT *
FROM dbo.Test;

Nel caso ciò sembri sorprendente, ricorda che mostrare le righe come eliminate significherebbe visualizzare una vista non vincolata dei dati, che non è consentita in caso di isolamento in lettura.

Il test di eliminazione

Nonostante il successo del test selezionato, un tentativo di eliminare queste stesse righe della sessione corrente verranno bloccate. Potresti immaginare che questo blocco si verifichi quando l'operazione tenta di acquisire esclusiva serrature, ma non è così.

L'eliminazione non utilizza il controllo delle versioni delle righe individuare le righe da eliminare; tenta invece di acquisire blocchi di aggiornamento. I blocchi di aggiornamento sono incompatibili con i blocchi di riga esclusivi mantenuti dalla sessione con la transazione aperta, quindi la query si blocca:

DELETE dbo.Test 
WHERE RowID IN (1, 2);

Il piano di query stimato per questa istruzione mostra che le righe da eliminare sono identificate da un'operazione di ricerca regolare prima che un operatore separato esegua l'eliminazione effettiva:

Possiamo vedere i blocchi mantenuti in questa fase eseguendo la stessa query di blocco di prima (da un'altra sessione) ricordandoci di modificare il riferimento SPID a quello utilizzato dalla query bloccata. I risultati si presentano così:

La nostra query di eliminazione è bloccata presso l'operatore Clustered Index Seek, che è in attesa di acquisire un blocco di aggiornamento da leggere dati. Ciò mostra che l'individuazione delle righe da eliminare in RCSI acquisisce blocchi di aggiornamento anziché leggere dati con versione potenzialmente obsoleta. Mostra inoltre che il blocco non è dovuto alla parte di eliminazione dell'operazione in attesa di acquisire un blocco esclusivo.

Il test di aggiornamento

Annulla la query bloccata e prova invece il seguente aggiornamento:

UPDATE dbo.Test
SET Data = Data + 1000
WHERE RowID IN (1, 2);

Il piano di esecuzione stimato è simile a quello visto nel test di eliminazione:

Il calcolo scalare serve a determinare il risultato dell'aggiunta di 1000 al valore corrente della colonna Dati in ogni riga, che viene letto da Ricerca indice cluster. Anche questa istruzione bloccherà quando eseguito, a causa del blocco dell'aggiornamento richiesto dall'operazione di lettura. Lo screenshot seguente mostra i blocchi mantenuti quando la query si blocca:

Come prima, la query viene bloccata alla ricerca, in attesa che venga rilasciato il blocco esclusivo incompatibile in modo da poter acquisire un blocco di aggiornamento.

Il test di inserimento

Il test successivo presenta un'istruzione che inserisce una nuova riga nella nostra tabella di test, utilizzando il valore della colonna Dati dalla riga esistente con ID 1 nella tabella. Ricordiamo che questa riga è ancora bloccata esclusivamente dalla sessione con la transazione aperta:

INSERT dbo.Test
    (RowID, Data)
SELECT 3, Data
FROM dbo.Test
WHERE RowID = 1;

Il piano di esecuzione è di nuovo simile ai test precedenti:

Questa volta, la query non è bloccata . Ciò mostra che i blocchi di aggiornamento non sono stati acquisiti durante la lettura dati per l'inserto. Questa query ha invece utilizzato il controllo delle versioni delle righe per acquisire il valore della colonna Dati per la riga appena inserita. I blocchi di aggiornamento non sono stati acquisiti perché questa istruzione non ha individuato alcuna riga da modificare , legge semplicemente i dati da utilizzare nell'inserto.

Possiamo vedere questa nuova riga nella tabella usando la query di test di selezione di prima:

Tieni presente che siamo in grado di aggiornare ed eliminare la nuova riga (che richiederà i blocchi di aggiornamento) perché non esiste un blocco esclusivo in conflitto. La sessione con la transazione aperta ha blocchi esclusivi solo sulle righe 1 e 2:

-- Update the new row
UPDATE dbo.Test
SET Data = 9999
WHERE RowID = 3;
-- Show the data
SELECT * FROM dbo.Test;
-- Delete the new row
DELETE dbo.Test
WHERE RowID = 3;

Questo test conferma che le istruzioni di inserimento non acquisiscono blocchi di aggiornamento durante la lettura , perché a differenza degli aggiornamenti e delle eliminazioni non modificano una riga esistente. La parte di lettura di un inserto utilizza il normale comportamento di controllo delle versioni delle righe RCSI.

Test di riferimento multiplo

Ho accennato in precedenza che solo il riferimento alla singola tabella utilizzato per individuare le righe da modificare acquisisce i blocchi di aggiornamento; altre tabelle nella stessa istruzione update o delete continuano a leggere le versioni delle righe. Come caso speciale di quel principio generale, un'istruzione di modifica dei dati con più riferimenti alla stessa tabella applica i blocchi di aggiornamento solo su un'istanza utilizzato per individuare le righe da modificare. Questo test finale illustra passo dopo passo questo comportamento più complesso.

La prima cosa di cui avremo bisogno è una nuova terza riga per la nostra tabella di test, questa volta con uno zero nella colonna Dati:

INSERT dbo.Test
    (RowID, Data)
VALUES
    (3, 0);

Come previsto, questo inserimento procede senza blocchi, risultando in una tabella simile a questa:

Ricorda, la seconda sessione è ancora esclusiva si blocca sui ferri 1 e 2 a questo punto. Siamo liberi di acquisire lucchetti sulla riga 3, se necessario. La seguente query è quella che useremo per mostrare il comportamento con più riferimenti alla tabella di destinazione:

-- Multi-reference update test
UPDATE WriteRef
SET Data = ReadRef.Data * 2
OUTPUT 
    ReadRef.RowID, 
    ReadRef.Data,
    INSERTED.RowID AS UpdatedRowID,
    INSERTED.Data AS NewDataValue
FROM dbo.Test AS ReadRef
JOIN dbo.Test AS WriteRef
    ON WriteRef.RowID = ReadRef.RowID + 2
WHERE 
    ReadRef.RowID = 1;

Questa è una query più complessa, ma il suo funzionamento è relativamente semplice. Ci sono due riferimenti alla tabella di test, uno che ho alias come ReadRef e l'altro come WriteRef. L'idea è di leggere dalla riga 1 (usando una versione di riga) tramite ReadRef e per aggiornamento la terza riga (che avrà bisogno di un blocco di aggiornamento) utilizzando WriteRef.

La query specifica la riga 1 in modo esplicito nella clausola where per il riferimento alla tabella di lettura. Si unisce al riferimento scritto alla stessa tabella aggiungendo 2 a quel RowID (identificando così la riga 3). L'istruzione di aggiornamento utilizza anche una clausola di output per restituire un set di risultati che mostra i valori letti dalla tabella di origine e le modifiche risultanti apportate alla riga 3.

Il piano di query stimato per questa istruzione è il seguente:

Le proprietà della ricerca etichettate (1) mostra che questa ricerca è su ReadRef alias, leggendo i dati dalla riga con RowID 1:

Questa operazione di ricerca non individua una riga che verrà aggiornata, quindi i blocchi di aggiornamento non presa; la lettura viene eseguita utilizzando dati versionati. La lettura non è bloccata dai lock esclusivi detenuti dall'altra sessione.

Lo scalare di calcolo etichettato (2) definisce un'espressione etichettata 1004 che calcola il valore aggiornato della colonna Dati. L'espressione 1009 calcola l'ID riga da aggiornare (1 + 2 =ID riga 3):

La seconda ricerca è un riferimento alla stessa tabella (3). Questa ricerca individua la riga che verrà aggiornata (riga 3) utilizzando l'espressione 1009:

Poiché questa ricerca individua una riga da modificare, un blocco aggiornamento viene preso invece di utilizzare le versioni di riga. Non esiste un blocco esclusivo in conflitto sull'ID riga 3, quindi la richiesta di blocco viene concessa immediatamente.

L'ultimo operatore evidenziato (4) è l'operazione di aggiornamento stessa. Il blocco degli aggiornamenti sulla riga 3 viene aggiornato a un esclusivo lock a questo punto, appena prima che la modifica venga effettivamente eseguita. Questo operatore restituisce anche i dati specificati nella clausola di output della dichiarazione di aggiornamento:

Il risultato dell'istruzione di aggiornamento (generata dalla clausola di output) è mostrato di seguito:

Lo stato finale della tabella è il seguente:

Possiamo confermare i blocchi presi durante l'esecuzione utilizzando una traccia Profiler:

Questo mostra che solo un singolo aggiornamento viene acquisito il blocco dei tasti di fila. Quando questa riga raggiunge l'operatore di aggiornamento, il blocco viene convertito in un esclusivo serratura. Alla fine della dichiarazione, il blocco viene rilasciato.

Potresti essere in grado di vedere dall'output di traccia che il valore hash di blocco per la riga bloccata dall'aggiornamento è (98ec012aa510) nel mio database di prova. La query seguente mostra che questo hash di blocco è effettivamente associato a RowID 3 nell'indice cluster:

SELECT RowID, %%LockRes%%
FROM dbo.Test;

Nota che i blocchi di aggiornamento presi in questi esempi hanno una durata più breve rispetto ai blocchi di aggiornamento presi se specifichiamo un UPDLOCK suggerimento. Questi blocchi di aggiornamento interni vengono rilasciati alla fine dell'istruzione, mentre UPDLOCK i blocchi vengono mantenuti fino alla fine della transazione.

Ciò conclude la dimostrazione dei casi in cui RCSI acquisisce i blocchi di aggiornamento per leggere i dati correnti sottoposti a commit invece di utilizzare il controllo delle versioni delle righe.

Bloccaggi condivisi e a portata di chiave sotto RCSI

Esistono numerosi altri scenari in cui il motore di database può ancora acquisire blocchi in RCSI. Queste situazioni si riferiscono tutte alla necessità di preservare la correttezza che sarebbe minacciata dall'affidarsi a dati con versione potenzialmente non aggiornati.

Blocchi condivisi presi per la convalida della chiave esterna

Per due tabelle in una relazione di chiave esterna diretta, il motore di database deve adottare misure per garantire che i vincoli non vengano violati basandosi su letture con versione potenzialmente obsoleta. L'attuale implementazione esegue questa operazione passando al blocco della lettura confermata quando si accede ai dati nell'ambito di un controllo automatico della chiave esterna.

L'adozione di blocchi condivisi garantisce che il controllo di integrità legga gli ultimi dati impegnati (non una versione precedente) o blocchi a causa di una modifica in corso simultanea. Il passaggio al blocco della lettura con commit si applica solo al particolare metodo di accesso utilizzato per controllare i dati della chiave esterna; altri accessi ai dati nella stessa istruzione continuano a utilizzare le versioni di riga.

Questo comportamento si applica solo alle istruzioni che modificano i dati, in cui la modifica influisce direttamente su una relazione di chiave esterna. Per le modifiche alla tabella di riferimento (principale), ciò significa aggiornamenti che influiscono sul valore di riferimento (a meno che non sia impostato su NULL ) e tutte le eliminazioni. Per la tabella di riferimento (figlio), questo significa tutti gli inserimenti e gli aggiornamenti (di nuovo, a meno che il riferimento alla chiave non sia NULL ). Le stesse considerazioni valgono per gli effetti dei componenti di un MERGE .

Di seguito viene mostrato un piano di esecuzione di esempio che mostra una ricerca di una chiave esterna che accetta blocchi condivisi:

Serializzabile per chiavi esterne in cascata

Laddove la relazione di chiave esterna ha un'azione a cascata, la correttezza richiede un'escalation locale alla semantica di isolamento serializzabile. Ciò significa che vedrai i blocchi dell'intervallo di chiavi presi per un'azione referenziale a cascata. Come nel caso dei blocchi di aggiornamento visti in precedenza, questi blocchi di intervallo di chiavi hanno l'ambito dell'istruzione, non della transazione. Di seguito è riportato un piano di esecuzione di esempio che mostra dove vengono presi i blocchi serializzabili interni in RCSI:

Altri scenari

Esistono molti altri casi specifici in cui il motore estende automaticamente la durata dei blocchi o aumenta localmente il livello di isolamento per garantire la correttezza. Questi includono la semantica serializzabile utilizzata quando si mantiene una vista indicizzata correlata o quando si mantiene un indice con IGNORE_DUP_KEY set di opzioni.

Il messaggio da asporto è che RCSI riduce la quantità di blocco, ma non può sempre eliminarlo del tutto.

La prossima volta

Il prossimo post di questa serie esamina il livello di isolamento dello snapshot.

[Vedi l'indice per l'intera serie]