Concorrenza e consistenza in Oracle

Siccome ormai si avvicinano le mie vacanze e per due settimane starò lontano dai computer, scrivo un post riportando degli appunti che scrissi ormai circa 3 anni e mezzo fa, l’argomento è in qualche modo collegato al post precedente, in cui ho parlato della struttura interna dei blocchi dati oracle, di ITL e di lock byte.

La gestione della concorrenza ha un ruolo fondamentale nel determinare la scalabilità delle applicazioni che si appoggiano su un sistema di gestione di  database. La gestione della concorrenza influisce anche sulla gestione della consistenza che in oracle è basata quella che viene chiamato  multi-versionamento dei dati (multi versioning).

Concorrenza

Molte logiche di elaborazione dei dati richiedono che determinate operazioni sugli stessi dati siano serializzate, questo al fine di garantire la correttezza logica delle stesse operazioni. D’altro canto però la serializzazione inibisce la scalabilità, ovvero il numero di operazioni che possono essere fatte per unità di tempo.

Oracle ha un sistema molto sofisticato di gestione della concorrenza che riduce al minimo la serializzazione al fine di massimizzare la scalabilità. Questo è stato possibile introducendo anche un meccanismo per la gestione della consitenza basato sulla gestione multi-versione dei dati.

Infatti, detto in breve, se una sessione modifica dati su una tabella Oracle blocca solo i record interessati, cosìcchè finche la sessione non committa non è possibile che un’altra sessione modifichi contemporaneamente gli stessi record.  In questo caso una sessione che volesse modificare gli stessi record rimarrebbe in attesa. Questo però non impedisce che altre sessioni interroghino le stesse tabelle che in quel momento la sessione sta aggiornando. Questo nonostante l’isolation level fornito da Oracle sia read committed, cioè le altre sessioni vedono solo i dati risultanti  da transazioni commitatte. Quindi se una sessione a fatto un update su un record e non ha committato, le altre sessioni vedono il record com’era prima dell’update.  Questo perchè Oracle quando viene fatto l’update modifica effettivamente i dati, però salva la versione precedente in quella struttura dati che fino alle versioni pre-9i era chiamata Rollback Segment e dalla versione 9i viene chiamata Undo. Quindi per fare vedere i dati in modo consistente Oracle li ricostruisce al volo con le informazioni salvate nell’undo.

Come funziona una transazione

Quando una transazione inizia acquisisce uno slot nella tabella delle transazioni di un segmento di rollback (undo), tale tabella si trova nell’header del segmento ed ogni slot contiene un puntatore all’ultimo extent in cui si trovano i dati di UNDO. Ogni transazione ha un identificatore univoco che è la combinazione del numero del segmento di rollback  e dello slot della tabella delle transazioni (ricordo che una transazione non può stare su più segmenti di rollback).

Un DML Lock acquisisce almeno due lock: uno “shared TM” sulla tabella (serve a far si che non venga modificata la struttura della tabella o non venga rimossa) un “exclusive TX” che è usato per implementare i lock per riga (ovvero, se un’altra sessione ha bisogno di modificare la riga si mette in coda su questo lock e enqueue) su tutte le righe che la transazione sta modificando. Il lock sulla riga viene registrato settando  un “lock byte” che si trova nell’intestazione delle riga stessa.

Nota:

Il lock byte punta ad uno slot nella Interested Transaction List (ITL) che si trova sempre nello header del data block. L’identificativo univoco della transazione (XID) è memorizzato nello slot dell’ITL (insieme all’SCN) per ogni riga lockata dalla transazione. Il lock al livello di riga  può essere solo in modalità esclusiva e una transazione può avere un numero illimitato di righe bloccate.

Riporto ora un pezzo un di dump di un blocco come esempio:


*** SESSION ID:(38.2363) 2005-11-15 09:18:07.000
Start dump data blocks tsn: 4 file#: 10 minblk 16597 maxblk 16597
buffer tsn: 4 rdba: 0x028040d5 (10/16597)
scn: 0x0000.2912e6c2 seq: 0x01 flg: 0x00 tail: 0xe6c20601
frmt: 0x02 chkval: 0x0000 type: 0x06=trans data
Block header dump:  0x028040d5
 Object id on Block? Y
 seg/obj: 0x2761  csc: 0x00.2912e6c0  itc: 2  flg: E  typ: 1 - DATA
 brn: 0  bdba: 0x28040d1 ver: 0x01
 inc: 0  exflg: 0

 Itl           Xid                  Uba         Flag  Lck        Scn/Fsc
0x01   0x0002.014.0001b8c4  0x008003ba.2f14.46  C---    0  scn 0x0000.264a3344
0x02   0x0005.01d.0001703e  0x0080004c.1cce.31  C---    0  scn 0x0000.1e5a5b1b

data_block_dump,data header at 0x8a41064
===============
tsiz: 0x1f98
hsiz: 0x54
pbl: 0x08a41064
bdba: 0x028040d5
 76543210
flag=--------
ntab=1
nrow=33
frre=-1
fsbo=0x54
fseo=0x182e
avsp=0x1a3b
tosp=0x1a3b
0xe:pti[0]    nrow=33    offs=0

tab 0, row 12, @0x19f8
tl: 39 fb: --H-FL-- lb: 0x0  cc: 7
col  0: [ 2]  c1 0b
col  1: [ 2]  c1 25
col  2: [12]  4f 50 45 52 41 54 4f 52 45 20 43 43
col  3: [ 2]  c1 02
col  4: [ 1]  31
col  5: [ 7]  78 66 09 14 0b 30 02
col  6: [ 3]  c2 04 45

*** 2005-11-15 09:18:18.000
Start dump data blocks tsn: 4 file#: 10 minblk 16597 maxblk 16597
buffer tsn: 4 rdba: 0x028040d5 (10/16597)
scn: 0x0000.2912e7cc seq: 0x01 flg: 0x00 tail: 0xe7cc0601
frmt: 0x02 chkval: 0x0000 type: 0x06=trans data
Block header dump:  0x028040d5
 Object id on Block? Y
 seg/obj: 0x2761  csc: 0x00.2912e6c0  itc: 2  flg: E  typ: 1 - DATA
 brn: 0  bdba: 0x28040d1 ver: 0x01
 inc: 0  exflg: 0

 Itl           Xid                  Uba         Flag  Lck        Scn/Fsc
0x01   0x0002.014.0001b8c4  0x008003ba.2f14.46  C---    0  scn 0x0000.264a3344
0x02   0x0006.025.0001e42d  0x00800139.281c.17  ----    1  fsc 0x0000.00000000

data_block_dump,data header at 0x8a41064
===============
tsiz: 0x1f98
hsiz: 0x54
pbl: 0x08a41064
bdba: 0x028040d5
 76543210
flag=--------
ntab=1
nrow=33
frre=-1
fsbo=0x54
fseo=0x182e
avsp=0x1a3b
tosp=0x1a3b
0xe:pti[0]    nrow=33    offs=0

tab 0, row 12, @0x19f8
tl: 39 fb: --H-FL-- lb: 0x2  cc: 7
col  0: [ 2]  c1 0b
col  1: [ 2]  c1 25
col  2: [12]  4f 50 45 52 41 54 4f 52 45 20 43 43
col  3: [ 2]  c1 02
col  4: [ 1]  31
col  5: [ 7]  78 69 0b 0f 0a 13 10
col  6: [ 3]  c2 04 45

Quando  la transazione committa l’informazione sui lock non viene spianata nei datablock, cio’ avviene quando un query successiva ripassa sul blocco. Questo meccanismo viene chiamato delayed block cleanout. In realtà questo accade se i blocchi vengo scritti sui datafile prima che la transazione venga committata. In particolare per ogni transazione oracle non mantiene nella cache tutti i blocchi se questi occupano più del 10% circa della dimensione della cache stessa, quindi se una transazione modifica un numero di blocchi che occupa oltre il 10% della cache prima che la transazione committi senz’altro alcuni di questi blocchi verranno scritti su disco e saranno soggetti a delayed block cleanout.  I blocchi rimasti in cache invece vengono “puliti” nel momento in cui viene fatto il commit. La query che fa il cleanout controlla lo stato della transazione e l’SCN nella tabella delle transazioni dell’header del segmento di rollback. Ecco una descrizione (ho annotato che è di Mark Bobak, credo fosse su un forum del metalink, ma ora non ho l’URL esatto) di come avviene il locking a livello di riga:

  1. la transazione inizia: sia esplicitamente via “set transaction …” o implicitamente, a causa di una qualunque istruzione DML.
  2. Viene allocata uno slot nell’header un segmento di rollback:  il segmento di rollback viene selezionato o con meccanismo round-robin o esplicitamente (gestione manuale dei rollback segments) via “set transaction use rollback segment …”
  3. Lo statement viene eseguito, vengono identificati i blocchi da cambiare: questo avviene tramite il piano di esecuzione, utilizzando gli access path li specificati. Per ogni  blocco identificato le righe vengono prese come segue:
  4. Ora la transazione cerca uno slot libero nell’ITL e lo  alloca, mettendoci il puntatore all’header del segemento di
    rollback precedentemente allocato.
  5. Una volta allocato uno slot nell’ITL le righe interessate devono essere marcate come lockate. Questo avviene nella sezione dell’header del datablock “row directory” (nota, questo non è vero, il lock byte fa parte dell’instestazioen della singola riga e non si trova nella row direcot0ry, vedere commenti sotto). Il lock byte viene settato per puntare allo slot nell’ITL di questa transazione.
  6. Alla fine un commit o un rollback rilascerà il lock. In caso di commit, l’unica cosa che accadrà sarà che nella
    tabella delle transazioni nell’header del segmento di rollback verrà registrato che la transazione è committata.

E’ interessante notare che nelle  informazioni di rollback viene salvato oltre allo slot dell’ITL anche lo slot dell’header del rollback segment (docid 94589.999  metalink, da verificare) che poi viene soprascritto, questo puo’ permettere di recuperare le informazioni di tale slot, perché potrebbe essere stato soprascritto lo slot ma non l’extent usato.

Inoltre, come sottolineato anche in un’interessante documento di Connor McDonald (anche di questo ora non il riferimento),  i blocchi che compongono Header dei segment di UNDO vengono trattati come ogni altro blocco nel database, quindi Oracle è in grado di fare l”undo” di tali blocchi. Immagino che non sia fattibile fare altrettanto per i blocchi di UNDO veri e propri perché la dimensione aumenterebbe esponenzialmente, cioè se quando creo un segmento di UNDO devo salvare prima il contenuto dell’undo precendete creo segmenti di undo che via via crescono sempre.

Consistenza

Quando una query inizia l’esecuzione, l’SCN corrente viene registrato per stabilire l’istante temporale di inizio della query. Questo SCN viene poi confrontato con l’SCN del blocco dati estratto per recuperare i dati richiesti. Questo confronto serve a stabilire se i dati contenuti nel blocco sono consistenti ovvero che non siano stati modificati dopo l’inizio della query. Se l’SCN del blocco è più piccolo, allora i dati nel blocco non sono stati toccati dopo l’inizio della query  e quindi sono consistenti. Se l’SCN del blocco è più grande dell’SCN salvato viene creata in cache una versione del blocco (chiamata CR constant read o consistent read) applicando elementi di Undo al blocco fino a riportarlo ad una versione con SCN più piccolo di quello salvato. Una volta applicati gli elementi di Undo Oracle ricontrolla l’SCN, se è ancora più grande di quello salvato Oracle riesamina l’ITL (ricostruita anch’essa tramite l’applicazione di undo) e reitera il processo di applicazione dell’undo.  Come già detto la stessa cosa Oracle può farla con i blocchi che compongono l’header del segmento di undo interessato.

Quando scrissi questi appunti utilizzavo solo Oracle 9, si può notare dal dump, infatti nella versione 10g il dump contiene anche il dump esadecimale oltre a quello codificato. Ho scoperto solo poco tempo, sul sito di Julian Dyke, che è possibile fare il dump in esadecimale anche sulla 9i.  Sul dump in esadecimale mi sono cimentato in questi giorni comprendendo meglio il discorso “little endian”, che rende la vita alquanto difficile , ma questa è un’altra storia e forse l’argomento del prossimo post.

9 pensieri su “Concorrenza e consistenza in Oracle

  1. Roberto

    Quest’argomento è sempre stato un bordello, e anch’io mi sono cimentato a più riprese nel cercare di districarlo. Rileggerò con attenzione (quando avrò più tempo) il tuo post, con l’intenzione di cercare di rendere congruenti (consistenti in linguaggio Oracle) quello che ho capito io con quello che dici tu.
    Da una prima lettura non mi torna il punto 5): “Una volta allocato uno slot nell’ITL…”.
    Ogni entry (o slot) della row directory contiene solo l’indirizzo fisico (offset rispetto all’inizio del blocco) della riga, nella row directory non c’è l’informazione di lock.
    L’informazione di lock è nell’header della riga (in uno dei primi 3 bytes di ogni riga) insieme al puntamento allo slot dell’ITL.

    1. Esatto Roberto, per me è stato un po’ arduo e la mia testardaggine mi ci ha fatto perdere tempo. Riguardo al punto 5 in particolare hai ragione, questo è stato un punto su cui ho sbattuto la testa (per mio masochismo) in quanto non sapevo fare il dump in esadecimale e non mi fidavo completamente del dump strutturato. Ho ricercato la fonte da cui avevo preso quella lista, ho trovato due indirizzi:
      1) http://www.mi-oaug.org/Presentations/Understanding%20Locks%20and%20Enqueues.ppt
      2) http://oracle-abc.wikidot.com/row-level-locking-mechanism
      Ora, studiando il dump in esadecimale ho toccato con mano e visto con i miei occhi che è come dici tu.

      1. Roberto

        Ahia, ho visto la fonte da cui hai attinto… c’era da aspettarselo 😉
        Il tuo approccio al dump esadecimale è assolutamente quello raccomandabile… avendo tempo da perderci.
        Le internals sono intriganti ma spesso inutili, e sono sempre release dependent, ma però dò sempre uno sguardo (anzi più di uno) quando se ne parla, e se a parlarne è J. Lewis sono oro colato. Ad esempio dopo averti scritto sono ritornato al post “http://jonathanlewis.wordpress.com/2009/05/21/row-directory/” e lì c’era il dettaglio assolutamente da guru:
        […]
        As I delete a row it is first marked as deleted (the “flag” byte gets the D bit set) and eventually, when the block needs to be tidied and re-packed to make available space usable, the deleted rows are trimmed back to a stub of just two bytes (the flag byte and the lock byte).
        […]
        Dal finale si può dedurre che il lock byte è proprio nella riga, non nella row directory.

  2. Ebbene, ho trovato il sito dal quale probabilemente a suo tempo trassi la descrizione, e manco a dirlo, pur non essendo operara sua, è il sito di Burleson, che più volte si è scontratto con Lewis; i due hanno due approcci (e quindi attendibilità) decisamente diversi. Nella frase che riporti di Lewis è riassunto in quattro righe un meccanismo veramente interessante e complesso che in effetti ho verificato con degli esperimenti. In effetti io mi aspettavo che anche le operazioni di riordino e re-impacchettamento venissero fatte con il delayed cleanup. Invece anche in questo caso, oracle rimanda le operazioni a quando servono veramente.

  3. Roberto

    Ho riletto come mi ero ripromesso quanto sopra. Vorrei segnalarti alcuni passi che non mi tornano:
    1) ” In particolare per ogni transazione oracle non mantiene nella cache tutti i blocchi se questi occupano più del 10% circa della dimensione della cache stessa, quindi se una transazione modifica un numero di blocchi che occupa oltre il 10% della cache prima che la transazione committi senz’altro alcuni di questi blocchi verranno scritti su disco e saranno soggetti a delayed block cleanout.”

    Io la so così: l’indirizzo (DBA) di tutti i blocchi modificati da una transazione viene registrato in una lista che può mappare al più il 10% della Buffer Cache.
    Alcuni di questi buffers durante la transazione possono essere scaricati su disco, per esigenze di caching.
    Al momento del commit questa lista viene scorsa in senso inverso, e nei blocchi letti viene effettuato il “commit cleanout” (ovvero viene scritta l’informazione del “commit SCN” nell’ITL relativa alla transazione), questo però solo nei blocchi che non hanno subito il flush sopra accennato.
    Per transazioni massive (che modificano un numero di blocchi superiore al 10% della cache, lunghezza massima della lista) non si riescono a memorizzare i DBAs di tutti i blocchi coinvolti, e quindi tutti quelli non riferiti nella lista più quelli flushed non saranno sottoposti a commit cleanout.
    Questi saranno i blocchi/buffers che saranno soggetti al delayed block cleanout.

    2) “I blocchi rimasti in cache invece vengono “puliti” nel momento in cui viene fatto il commit. La query che fa il cleanout controlla lo stato della transazione e l’SCN nella tabella delle transazioni dell’header del segmento di rollback.”

    Per questi buffers viene eseguito solo un parziale “block cleanout”: il “commit cleanout”, che come sopra detto è la scrittura nell’ITL interessata del commit SCN… che si è appena scritto nella transaction table e quindi è già disponibile.
    La lettura dell’SCN dalla tabella delle transazioni (nell’header del segmento di rollback/undo) è necessaria per il “delayed block cleanout”, che viene svolto da una sessione successiva, per i blocchi su cui non si è effettuato il “commit cleanout” e quindi manca nel blocco l’informazione del commit SCN.

  4. Nicola

    Ciao,
    Spero il thread sia ancora attivo. Io ho un problema che volevo sottoporvi, che provo a spiegare sotto:
    Con una semplice query vado in lettura da una tabella remota (accedo via dblink) per replicare circa 1ml di record. Solitamente impega qualche secondo, ma alcune volte impiega circa 5 ore. in altri casi ho dovuto killare la query.
    So con certezza che accade ciò quando gira il processo che va in scrittura su questa tabella (sia insert che update).
    Allora mi domando se alla luce di quando detto circa la concorrenza non vi è un modo per fare in modoo che la lettura avvenga in modo più rapido, ovviamente pescando dalla vecchia fotografia del dato.
    Inoltre corro il rischio di infastidire il processo di scrittura (si tratta di un db sorgente che nn è sotto il nostro controllo) ?

    Grazie per l’aiuto.
    nicola.

Scrivi una risposta a Nicola Cancella risposta