Transazioni in InterSystems IRIS globali

Transazioni in InterSystems IRIS globaliIl DBMS InterSystems IRIS supporta strutture interessanti per l'archiviazione dei dati: globali. Si tratta essenzialmente di chiavi multilivello con diverse funzionalità aggiuntive sotto forma di transazioni, funzioni veloci per l'attraversamento di alberi di dati, blocchi e un proprio linguaggio ObjectScript.

Maggiori informazioni sui globali nella serie di articoli "I globali sono spade preziose per l'archiviazione dei dati":

Alberi. Parte 1
Alberi. Parte 2
Array sparsi. Parte 3

Mi sono interessato a come vengono implementate le transazioni nelle globali, quali funzionalità ci sono. Dopotutto, questa è una struttura completamente diversa per l'archiviazione dei dati rispetto alle solite tabelle. Livello molto più basso.

Come è noto dalla teoria dei database relazionali, una buona implementazione delle transazioni deve soddisfare i requisiti ACIDO:

A - Atomico (atomicità). Vengono registrate tutte le modifiche apportate alla transazione o nessuna.

C – Coerenza. Una volta completata una transazione, lo stato logico del database deve essere internamente coerente. In molti sensi questo requisito riguarda il programmatore, ma nel caso dei database SQL riguarda anche le chiavi esterne.

Io - Isolare. Le transazioni eseguite in parallelo non dovrebbero influenzarsi a vicenda.

D – Durevole. Dopo il completamento con successo di una transazione, i problemi ai livelli inferiori (ad esempio un'interruzione di corrente) non dovrebbero influenzare i dati modificati dalla transazione.

I globali sono strutture dati non relazionali. Sono stati progettati per funzionare molto velocemente su hardware molto limitato. Diamo un'occhiata all'implementazione delle transazioni in globali utilizzando immagine docker ufficiale di IRIS.

Per supportare le transazioni in IRIS, vengono utilizzati i seguenti comandi: INIZIA, TCOMMIT, TROLLBACK.

1. Atomicità

Il modo più semplice per verificare è l'atomicità. Controlliamo dalla console del database.

Kill ^a
TSTART
Set ^a(1) = 1
Set ^a(2) = 2
Set ^a(3) = 3
TCOMMIT

Quindi concludiamo:

Write ^a(1), “ ”, ^a(2), “ ”, ^a(3)

Otteniamo:

1 2 3

Va tutto bene. L'atomicità viene mantenuta: tutti i cambiamenti vengono registrati.

Complichiamo il compito, introduciamo un errore e vediamo come la transazione viene salvata, parzialmente o per niente.

Controlliamo nuovamente l'atomicità:

Kill ^A
TSTART
Set ^a(1) = 1
Set ^a(2) = 2
Set ^a(3) = 3

Quindi fermeremo con la forza il container, lo lanceremo e vedremo.

docker kill my-iris

Questo comando è quasi equivalente a uno spegnimento forzato, poiché invia un segnale SIGKILL per interrompere immediatamente il processo.

Forse la transazione è stata parzialmente salvata?

WRITE ^a(1), ^a(2), ^a(3)
^
<UNDEFINED> ^a(1)

- No, non è sopravvissuto.

Proviamo il comando rollback:

Kill ^A
TSTART
Set ^a(1) = 1
Set ^a(2) = 2
Set ^a(3) = 3
TROLLBACK

WRITE ^a(1), ^a(2), ^a(3)
^
<UNDEFINED> ^a(1)

Neppure nulla è sopravvissuto.

2. Coerenza

Poiché nei database basati su globali, anche le chiavi vengono create su globali (permettetemi di ricordarvi che una globale è una struttura di archiviazione dei dati di livello inferiore rispetto a una tabella relazionale), per soddisfare il requisito di coerenza, è necessario includere una modifica nella chiave nella stessa transazione di un cambiamento nel globale.

Ad esempio, abbiamo un ^person globale, in cui memorizziamo le personalità e utilizziamo la TIN come chiave.

^person(1234567, ‘firstname’) = ‘Sergey’
^person(1234567, ‘lastname’) = ‘Kamenev’
^person(1234567, ‘phone’) = ‘+74995555555
...

Per poter effettuare una ricerca rapida per cognome e nome, abbiamo creato la chiave ^index.

^index(‘Kamenev’, ‘Sergey’, 1234567) = 1

Affinché il database sia coerente, dobbiamo aggiungere la persona in questo modo:

TSTART
^person(1234567, ‘firstname’) = ‘Sergey’
^person(1234567, ‘lastname’) = ‘Kamenev’
^person(1234567, ‘phone’) = ‘+74995555555
^index(‘Kamenev’, ‘Sergey’, 1234567) = 1
TCOMMIT

Di conseguenza, quando eliminiamo dobbiamo utilizzare anche una transazione:

TSTART
Kill ^person(1234567)
ZKill ^index(‘Kamenev’, ‘Sergey’, 1234567)
TCOMMIT

In altre parole, il rispetto del requisito di coerenza ricade interamente sulle spalle del programmatore. Ma quando si tratta di globali, questo è normale, a causa della loro natura di basso livello.

3. Isolamento

È qui che iniziano le terre selvagge. Molti utenti lavorano contemporaneamente sullo stesso database, modificando gli stessi dati.

La situazione è paragonabile a quando molti utenti lavorano contemporaneamente con lo stesso repository di codice e tentano di applicare contemporaneamente modifiche a più file contemporaneamente.

Il database dovrebbe sistemare tutto in tempo reale. Considerando che nelle aziende serie esiste addirittura una persona speciale responsabile del controllo della versione (unione di rami, risoluzione di conflitti, ecc.), e che il database deve fare tutto questo in tempo reale, la complessità del compito e la correttezza delle progettazione del database e codice che lo serve.

Il database non è in grado di comprendere il significato delle azioni eseguite dagli utenti per evitare conflitti se stanno lavorando sugli stessi dati. Può solo annullare una transazione in conflitto con un'altra o eseguirle in sequenza.

Un altro problema è che durante l'esecuzione di una transazione (prima di un commit), lo stato del database potrebbe essere incoerente, quindi è auspicabile che altre transazioni non abbiano accesso allo stato incoerente del database, cosa che si ottiene nei database relazionali in molti modi: creazione di istantanee, righe multi-versione e così via.

Quando eseguiamo transazioni in parallelo, per noi è importante che queste non interferiscano tra loro. Questa è la proprietà dell'isolamento.

SQL definisce 4 livelli di isolamento:

  • LEGGERE NON CONSIGLIATO
  • LEGGI IMPEGNATO
  • LETTURA RIPETIBILE
  • SERIALIZZABILE

Diamo un'occhiata a ciascun livello separatamente. I costi di implementazione di ciascun livello crescono in modo quasi esponenziale.

LEGGERE NON CONSIGLIATO - questo è il livello di isolamento più basso, ma allo stesso tempo il più veloce. Le transazioni possono leggere le modifiche apportate l'una dall'altra.

LEGGI IMPEGNATO è il livello successivo di isolamento, che è un compromesso. Le transazioni non possono leggere le reciproche modifiche prima del commit, ma possono leggere qualsiasi modifica apportata dopo il commit.

Se abbiamo una lunga transazione T1, durante la quale sono avvenuti commit nelle transazioni T2, T3 ... Tn, che hanno funzionato con gli stessi dati di T1, quando richiediamo dati in T1 otterremo ogni volta un risultato diverso. Questo fenomeno è chiamato lettura non ripetibile.

LETTURA RIPETIBILE — in questo livello di isolamento non si ha il fenomeno della lettura non ripetibile, poiché per ogni richiesta di lettura dei dati viene creato uno snapshot dei dati risultanti e quando riutilizzati nella stessa transazione, i dati dello snapshot si usa. Tuttavia, è possibile leggere dati fantasma a questo livello di isolamento. Si riferisce alla lettura di nuove righe aggiunte da transazioni parallele con commit.

SERIALIZZABILE — il massimo livello di isolamento. È caratterizzato dal fatto che i dati utilizzati in qualsiasi modo in una transazione (lettura o modifica) diventano disponibili ad altre transazioni solo dopo il completamento della prima transazione.

Per prima cosa, vediamo se esiste un isolamento delle operazioni in una transazione dal thread principale. Apriamo 2 finestre di terminale.

Kill ^t

Write ^t(1)
2

TSTART
Set ^t(1)=2

Non c'è isolamento. Un thread vede cosa sta facendo il secondo che ha aperto la transazione.

Vediamo se le transazioni di thread diversi vedono cosa sta succedendo al loro interno.

Apriamo 2 finestre di terminale e apriamo 2 transazioni in parallelo.

kill ^t
TSTART
Write ^t(1)
3

TSTART
Set ^t(1)=3

Le transazioni parallele vedono i dati degli altri. Quindi, abbiamo ottenuto il livello di isolamento più semplice, ma anche più veloce, READ UNCOMMITED.

In linea di principio, ciò sarebbe prevedibile per i titoli globali, per i quali la performance è sempre stata una priorità.

Cosa succederebbe se avessimo bisogno di un livello di isolamento più elevato nelle operazioni sui mercati globali?

Qui è necessario pensare al motivo per cui sono necessari i livelli di isolamento e a come funzionano.

Il livello di isolamento più alto, SERIALIZE, significa che il risultato delle transazioni eseguite in parallelo è equivalente alla loro esecuzione sequenziale, il che garantisce l'assenza di collisioni.

Possiamo farlo utilizzando i blocchi intelligenti in ObjectScript, che hanno molti usi diversi: puoi eseguire blocchi regolari, incrementali e multipli con il comando BLOCCO.

Livelli di isolamento inferiori sono compromessi progettati per aumentare la velocità del database.

Vediamo come possiamo ottenere diversi livelli di isolamento utilizzando le serrature.

Questo operatore consente di prendere non solo i lock esclusivi necessari per modificare i dati, ma i cosiddetti lock condivisi, che possono prendere più thread in parallelo quando devono leggere dati che non devono essere modificati da altri processi durante il processo di lettura.

Maggiori informazioni sul metodo di blocco a due fasi in russo e inglese:

Blocco a due fasi
Chiusura a due fasi

La difficoltà è che durante una transazione lo stato del database potrebbe essere incoerente, ma questi dati incoerenti sono visibili ad altri processi. Come evitarlo?

Utilizzando i blocchi, creeremo finestre di visibilità in cui lo stato del database sarà coerente. E tutti gli accessi a tali finestre di visibilità dello stato concordato saranno controllati da serrature.

I blocchi condivisi sugli stessi dati sono riutilizzabili: diversi processi possono accettarli. Questi blocchi impediscono ad altri processi di modificare i dati, ad es. vengono utilizzati per formare finestre con stato del database coerente.

Per le modifiche ai dati vengono utilizzati blocchi esclusivi: solo un processo può accettare tale blocco. Un blocco esclusivo può essere ottenuto da:

  1. Qualsiasi processo se i dati sono gratuiti
  2. Solo il processo che ha un blocco condiviso su questi dati ed è stato il primo a richiedere un blocco esclusivo.

Transazioni in InterSystems IRIS globali

Più stretta è la finestra di visibilità, più a lungo gli altri processi dovranno attendere, ma più coerente potrà essere lo stato del database al suo interno.

READ_COMMITTED — l'essenza di questo livello è che vediamo solo i dati impegnati da altri thread. Se i dati in un'altra transazione non sono ancora stati confermati, vediamo la loro versione precedente.

Questo ci consente di parallelizzare il lavoro invece di aspettare che il blocco venga rilasciato.

Senza accorgimenti particolari non potremo vedere la vecchia versione dei dati in IRIS, quindi dovremo accontentarci dei lucchetti.

Di conseguenza, dovremo utilizzare blocchi condivisi per consentire la lettura dei dati solo nei momenti di consistenza.

Supponiamo di avere una base utenti ^persone che trasferiscono denaro tra loro.

Momento del trasferimento dalla persona 123 alla persona 242:

LOCK +^person(123), +^person(242)
Set ^person(123, amount) = ^person(123, amount) - amount
Set ^person(242, amount) = ^person(242, amount) + amount
LOCK -^person(123), -^person(242)

Il momento della richiesta della somma di denaro alla persona 123 prima dell'addebito deve essere accompagnato da un blocco esclusivo (per impostazione predefinita):

LOCK +^person(123)
Write ^person(123)

E se devi mostrare lo stato dell'account nel tuo account personale, puoi utilizzare un blocco condiviso o non utilizzarlo affatto:

LOCK +^person(123)#”S”
Write ^person(123)

Tuttavia, se presupponiamo che le operazioni sul database vengano eseguite quasi istantaneamente (permettetemi di ricordarvi che i globali sono una struttura di livello molto inferiore rispetto a una tabella relazionale), la necessità di questo livello diminuisce.

LETTURA RIPETIBILE - Questo livello di isolamento consente più letture di dati che possono essere modificati da transazioni simultanee.

Di conseguenza, dovremo mettere un blocco condiviso sulla lettura dei dati che modifichiamo e blocchi esclusivi sui dati che modifichiamo.

Fortunatamente, l'operatore LOCK ti consente di elencare in dettaglio tutti i blocchi necessari, di cui possono essercene molti, in un'unica istruzione.

LOCK +^person(123, amount)#”S”
чтение ^person(123, amount)

altre operazioni (in questo momento i thread paralleli provano a cambiare ^person(123, amount), ma non ci riescono)

LOCK +^person(123, amount)
изменение ^person(123, amount)
LOCK -^person(123, amount)

чтение ^person(123, amount)
LOCK -^person(123, amount)#”S”

Quando si elencano i blocchi separati da virgole, vengono presi in sequenza, ma se si esegue questa operazione:

LOCK +(^person(123),^person(242))

poi vengono presi atomicamente tutti in una volta.

SERIALIZZAZIONE — dovremo impostare i blocchi in modo che alla fine tutte le transazioni che hanno dati comuni vengano eseguite in sequenza. Per questo approccio, la maggior parte dei blocchi dovrebbe essere esclusiva e adottata nelle aree più piccole del globale per le prestazioni.

Se parliamo di addebito di fondi nella ^persona globale, allora è accettabile solo il livello di isolamento SERIALIZE, poiché il denaro deve essere speso in modo rigorosamente sequenziale, altrimenti è possibile spendere lo stesso importo più volte.

4. Durabilità

Ho condotto test con taglio duro del contenitore utilizzando

docker kill my-iris

La base li ha tollerati bene. Non sono stati identificati problemi.

conclusione

Per le transazioni globali, InterSystems IRIS dispone del supporto per le transazioni. Sono veramente atomici e affidabili. Per garantire la coerenza di un database basato su valori globali, sono necessari gli sforzi del programmatore e l'uso di transazioni, poiché non dispone di costrutti integrati complessi come le chiavi esterne.

Il livello di isolamento dei globali senza utilizzare i lock è READ UNCOMMITED e quando si utilizzano i lock può essere garantito fino al livello SERIALIZE.

La correttezza e la velocità delle transazioni sui globali dipende molto dall'abilità del programmatore: più lock vengono utilizzati durante la lettura, più alto è il livello di isolamento, e più lock esclusivi vengono adottati, più veloci saranno le prestazioni.

Fonte: habr.com

Aggiungi un commento