SQL Server fornisce due implementazioni fisiche di read commit livello di isolamento definito dallo standard SQL, blocco dell'isolamento dello snapshot in lettura con commit e lettura con commit (RCSI ). Sebbene entrambe le implementazioni soddisfino i requisiti stabiliti nello standard SQL per i comportamenti di isolamento con commit di lettura, RCSI ha comportamenti fisici abbastanza diversi dall'implementazione di blocco che abbiamo esaminato nel post precedente di questa serie.
Garanzie logiche
Lo standard SQL richiede che una transazione che opera al livello di isolamento di lettura commit non subisca letture sporche. Un altro modo per esprimere questo requisito è dire che una transazione di lettura confermata deve incontrare solo dati confermati .
Lo standard dice anche che leggere le transazioni impegnate potrebbe sperimentare i fenomeni di concorrenza noti come letture non ripetibili e fantasmi (sebbene non siano effettivamente tenuti a farlo). A quanto pare, entrambe le implementazioni fisiche dell'isolamento con commit di lettura in SQL Server possono subire letture non ripetibili e righe fantasma, sebbene i dettagli precisi siano piuttosto diversi.
Una vista puntuale dei dati impegnati
Se l'opzione del database READ_COMMITTED_SNAPSHOT
in ON
, SQL Server utilizza un'implementazione del controllo delle versioni delle righe del livello di isolamento di lettura commit. Quando questa opzione è abilitata, le transazioni che richiedono l'isolamento read commit utilizzano automaticamente l'implementazione RCSI; non sono necessarie modifiche al codice T-SQL esistente per utilizzare RCSI. Nota attentamente però che questo non è lo stesso come dire che il codice si comporterà allo stesso modo sotto RCSI come quando si utilizza l'implementazione di blocco di read commit, in effetti in genere questo non è il caso .
Non c'è nulla nello standard SQL che richieda che i dati letti da una transazione con commit di lettura siano più recenti dati impegnati. L'implementazione RCSI di SQL Server sfrutta questo vantaggio per fornire alle transazioni una vista point-in-time di dati impegnati, dove quel momento è il momento in cui è iniziata la istruzione corrente esecuzione (non nel momento in cui è iniziata una transazione contenente).
Questo è abbastanza diverso dal comportamento dell'implementazione del blocco di SQL Server di read commit, in cui l'istruzione vede i dati con commit più recenti a partire dal momento in cui ogni elemento viene letto fisicamente . Il blocco della lettura con commit rilascia i blocchi condivisi il più rapidamente possibile, quindi l'insieme di dati rilevato potrebbe provenire da momenti molto diversi.
Per riassumere, il blocco della lettura confermata vede ogni riga com'era all'epoca, è stato brevemente bloccato e letto fisicamente; RCSI vede tutte le righe come erano al momento dell'inizio della dichiarazione. È garantito che entrambe le implementazioni non vedranno mai dati non vincolati, ma i dati che incontrano possono essere molto diversi.
Le implicazioni di una visione puntuale
Vedere una visione puntuale dei dati impegnati potrebbe sembrare evidentemente superiore al comportamento più complesso dell'implementazione del blocco. È chiaro, ad esempio, che una vista point-in-time non può soffrire dei problemi di righe mancanti o incontrare la stessa riga più volte , che sono entrambi possibili con il blocco dell'isolamento di lettura commit.
Un secondo importante vantaggio di RCSI è che non acquisisce lock condivisi durante la lettura dei dati, perché i dati provengono dall'archivio delle versioni di riga anziché essere accessibili direttamente. La mancanza di blocchi condivisi può migliorare notevolmente la concorrenza eliminando i conflitti con transazioni simultanee che cercano di acquisire blocchi incompatibili. Questo vantaggio è comunemente riassunto dicendo che i lettori non bloccano gli scrittori sotto RCSI e viceversa. Come ulteriore conseguenza della riduzione del blocco dovuto a richieste di blocco incompatibili, l'opportunità di deadlock di solito è notevolmente ridotto quando si esegue con RCSI.
Tuttavia, questi vantaggi non sono privi di costi e avvertenze . Per prima cosa, il mantenimento delle versioni delle righe impegnate consuma risorse di sistema, quindi è importante che l'ambiente fisico sia configurato per far fronte a questo, principalmente in termini di tempdb prestazioni e requisiti di memoria/spazio su disco.
Il secondo avvertimento è un po' più sottile:RCSI fornisce una vista istantanea dei dati impegnati com'erano all'inizio dell'istruzione, ma non c'è nulla che impedisca la modifica dei dati reali (e il commit di tali modifiche) durante l'esecuzione dell'istruzione RCSI. Non ci sono serrature condivise, ricorda. Una conseguenza immediata di questo secondo punto è che il codice T-SQL eseguito in RCSI potrebbe prendere decisioni basate su informazioni non aggiornate , rispetto all'attuale stato di commit del database. Ne parleremo di più a breve.
C'è un'ultima osservazione (specifica per l'implementazione) che voglio fare su RCSI prima di andare avanti. Funzioni scalari e multi-istruzione eseguire utilizzando un contesto T-SQL interno diverso dall'istruzione contenitore. Ciò significa che la vista point-in-time vista all'interno di una chiamata di funzione scalare o multi-istruzione può essere successiva alla vista point-in-time vista dal resto dell'istruzione. Ciò può comportare incongruenze impreviste, poiché parti diverse della stessa affermazione visualizzano dati da momenti diversi . Questo comportamento strano e confuso non si applicano alle funzioni in linea, che vedono la stessa istantanea dell'istruzione in cui appaiono.
Letture e fantasmi non ripetibili
Data una vista point-in-time a livello di istruzione dello stato di commit del database, potrebbe non essere immediatamente evidente come una transazione con commit di lettura in RCSI potrebbe subire i fenomeni di lettura non ripetibile o riga fantasma. In effetti, se limitiamo il nostro pensiero all'ambito di una singola affermazione , nessuno di questi fenomeni è possibile in RCSI.
Leggere gli stessi dati più volte all'interno della stessa istruzione sotto RCSI restituirà sempre gli stessi valori di dati, nessun dato scomparirà tra quelle letture e non appariranno nemmeno nuovi dati. Se ti stai chiedendo che tipo di istruzione potrebbe leggere gli stessi dati più di una volta, pensa alle query che fanno riferimento alla stessa tabella più di una volta, magari in una sottoquery.
La coerenza della lettura a livello di istruzione è un'ovvia conseguenza delle letture emesse rispetto a uno snapshot fisso dei dati. Il motivo per cui RCSI non fornire protezione da letture e fantasmi non ripetibili è che questi fenomeni standard SQL sono definiti a livello di transazione. Più istruzioni all'interno di una transazione in esecuzione su RCSI possono visualizzare dati diversi, perché ogni istruzione vede una vista temporale dal momento quella particolare affermazione iniziato.
Per riassumere, ogni affermazione all'interno di una transazione RCSI vede un set di dati statico impegnato, ma tale set può cambiare tra le istruzioni all'interno della stessa transazione.
Dati non aggiornati
La possibilità che il nostro codice T-SQL prenda una decisione importante sulla base di informazioni non aggiornate è più che un po' inquietante. Considera per un momento che lo snapshot point-in-time utilizzato da una singola istruzione eseguita in RCSI potrebbe essere arbitrariamente vecchio .
Un'istruzione che viene eseguita per un periodo di tempo considerevole continuerà a vedere lo stato di commit del database com'era all'inizio dell'istruzione. Nel frattempo, nell'istruzione mancano tutte le modifiche salvate che si sono verificate nel database da quel momento.
Questo non vuol dire che i problemi associati all'accesso a dati obsoleti in RCSI siano limitati a di lunga durata affermazioni, ma i problemi potrebbero essere certamente più pronunciati in questi casi.
Una questione di tempismo
Questo problema di dati non aggiornati si applica in linea di principio a tutte le dichiarazioni RCSI, indipendentemente dalla rapidità con cui potrebbero essere completate. Per quanto piccola sia la finestra temporale, c'è sempre la possibilità che un'operazione simultanea possa modificare il set di dati con cui stiamo lavorando, senza che noi siamo consapevoli di tale cambiamento. Esaminiamo di nuovo uno dei semplici esempi che abbiamo usato prima quando esploriamo il comportamento del blocco read commit:
INSERT dbo.OverdueInvoices SELECT I.InvoiceNumber FROM dbo.Invoices AS I WHERE I.TotalDue > ( SELECT SUM(P.Amount) FROM dbo.Payments AS P WHERE P.InvoiceNumber = I.InvoiceNumber );
Se eseguita sotto RCSI, questa affermazione non può vedere tutte le modifiche al database salvate che si verificano dopo l'avvio dell'esecuzione dell'istruzione. Sebbene non incontreremo i problemi di righe perse o incontrate moltiplicate possibili nell'ambito dell'implementazione del blocco, una transazione simultanea potrebbe aggiungere un pagamento che dovrebbe per evitare che a un cliente venga inviata una severa lettera di avvertimento in merito a un pagamento scaduto dopo l'inizio dell'esecuzione della dichiarazione di cui sopra.
Probabilmente puoi pensare a molti altri potenziali problemi che potrebbero verificarsi in questo scenario o in altri concettualmente simili. Più a lungo dura l'istruzione, più obsoleta diventa la sua visualizzazione del database e maggiore è la portata di possibili conseguenze indesiderate.
Naturalmente, ci sono molti fattori attenuanti in questo esempio specifico. Il comportamento potrebbe essere considerato perfettamente accettabile. Dopotutto, inviare una lettera di sollecito perché un pagamento è arrivato con qualche secondo di ritardo è un'azione facilmente difendibile. Il principio resta comunque.
Errori delle regole aziendali e rischi per l'integrità
Problemi più seri possono sorgere dall'uso di informazioni non aggiornate rispetto all'invio di una lettera di avvertimento con alcuni secondi di anticipo. Un buon esempio di questa classe di debolezza può essere visto con il codice trigger utilizzato per applicare una regola di integrità forse troppo complessa per essere applicata con vincoli di integrità referenziale dichiarativa. Per illustrare, considera il codice seguente, che utilizza un trigger per imporre una variazione di un vincolo di chiave esterna, ma che impone la relazione solo per determinate righe di tabella figlio:
ALTER DATABASE Sandpit SET READ_COMMITTED_SNAPSHOT ON WITH ROLLBACK IMMEDIATE; GO SET TRANSACTION ISOLATION LEVEL READ COMMITTED; GO CREATE TABLE dbo.Parent (ParentID integer PRIMARY KEY); GO CREATE TABLE dbo.Child ( ChildID integer IDENTITY PRIMARY KEY, ParentID integer NOT NULL, CheckMe bit NOT NULL ); GO CREATE TRIGGER dbo.Child_AI ON dbo.Child AFTER INSERT AS BEGIN -- Child rows with CheckMe = true -- must have an associated parent row IF EXISTS ( SELECT ins.ParentID FROM inserted AS ins WHERE ins.CheckMe = 1 EXCEPT SELECT P.ParentID FROM dbo.Parent AS P ) BEGIN RAISERROR ('Integrity violation!', 16, 1); ROLLBACK TRANSACTION; END END; GO -- Insert parent row #1 INSERT dbo.Parent (ParentID) VALUES (1);
Ora considera una transazione in esecuzione in un'altra sessione (usa un'altra finestra SSMS per questo se stai seguendo) che elimina la riga principale n. 1, ma non esegue ancora il commit:
SET TRANSACTION ISOLATION LEVEL READ COMMITTED; BEGIN TRANSACTION; DELETE FROM dbo.Parent WHERE ParentID = 1;
Nella nostra sessione originale, proviamo a inserire una riga figlio (selezionata) che fa riferimento a questo genitore:
INSERT dbo.Child (ParentID, CheckMe) VALUES (1, 1);
Il codice di attivazione viene eseguito, ma poiché RCSI vede solo commesso dati al momento dell'avvio dell'istruzione, vede ancora la riga padre (non l'eliminazione non vincolata) e l'inserimento ha esito positivo !
La transazione che ha eliminato la riga padre ora può eseguire il commit della modifica con successo, lasciando il database in modo incoerente stato in termini di nostra logica di innesco:
COMMIT TRANSACTION; SELECT P.* FROM dbo.Parent AS P; SELECT C.* FROM dbo.Child AS C;
Questo è ovviamente un esempio semplificato e potrebbe essere facilmente aggirato utilizzando le strutture di vincolo integrate. Regole aziendali molto più complesse e vincoli di pseudo-integrità possono essere scritti all'interno e al di fuori dei trigger . Il potenziale comportamento scorretto in RCSI dovrebbe essere ovvio.
Comportamento di blocco e ultimi dati impegnati
Ho accennato in precedenza che non è garantito che il codice T-SQL si comporti allo stesso modo in RCSI read commit come ha fatto usando l'implementazione del blocco. L'esempio di codice trigger precedente ne è un buon esempio, ma devo sottolineare che il problema generale non è limitato ai trigger .
RCSI in genere non è una buona scelta per qualsiasi codice T-SQL la cui correttezza dipende dal blocco se esiste una modifica simultanea senza commit. RCSI potrebbe anche non essere la scelta giusta se il codice dipende dalla lettura di corrente dati impegnati, piuttosto che gli ultimi dati impegnati al momento dell'inizio della dichiarazione. Queste due considerazioni sono correlate, ma non sono la stessa cosa.
Blocco lettura commessa sotto RCSI
SQL Server offre un modo per richiedere il blocco read commit quando RCSI è abilitato, utilizzando l'hint di tabella READCOMMITTEDLOCK
. Possiamo modificare il nostro trigger per evitare i problemi mostrati sopra aggiungendo questo suggerimento alla tabella che necessita di un comportamento di blocco per funzionare correttamente:
ALTER TRIGGER dbo.Child_AI ON dbo.Child AFTER INSERT AS BEGIN -- Child rows with CheckMe = true -- must have an associated parent row IF EXISTS ( SELECT ins.ParentID FROM inserted AS ins WHERE ins.CheckMe = 1 EXCEPT SELECT P.ParentID FROM dbo.Parent AS P WITH (READCOMMITTEDLOCK) -- NEW!! ) BEGIN RAISERROR ('Integrity violation!', 16, 1); ROLLBACK TRANSACTION; END END;
Con questa modifica in atto, il tentativo di inserire i blocchi di righe figlio potenzialmente orfani fino a quando la transazione di eliminazione non viene confermata (o interrotta). Se l'eliminazione viene eseguita, il codice di attivazione rileva la violazione di integrità e genera l'errore previsto.
Identificazione delle query che potrebbero non funzionare correttamente sotto RCSI è un compito non banale che potrebbe richiedere test approfonditi per avere ragione (e ricorda che questi problemi sono abbastanza generali e non si limitano al codice di attivazione!) Inoltre, aggiungendo il READCOMMITTEDLOCK
suggerimento a ogni tabella che ne ha bisogno può essere un processo noioso e soggetto a errori. Fino a quando SQL Server non fornirà un'opzione con un ambito più ampio per richiedere l'implementazione del blocco dove necessario, siamo bloccati con l'utilizzo dei suggerimenti per la tabella.
La prossima volta
Il prossimo post di questa serie continua il nostro esame dell'isolamento delle istantanee con commit in lettura, con uno sguardo al comportamento sorprendente delle dichiarazioni di modifica dei dati in RCSI.
[Vedi l'indice per l'intera serie]