Transazioni nei globali di InterSystems IRIS

Transazioni nei globali di InterSystems IRISIl DBMS InterSystems IRIS supporta strutture interessanti per la memorizzazione dei dati: i globali. Essi sono in sostanza chiavi multilivello con vari benefici, come transazioni, funzioni rapide per navigare gli alberi di dati, blocchi e il proprio linguaggio ObjectScript.

Scopri di più sui globali nella serie di articoli «I globali: spade e tesori per la memorizzazione dei dati»:

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

Sono curioso di come siano implementate le transazioni nei globali e quali caratteristiche presentino. Infatti, si tratta di una struttura di memorizzazione dei dati completamente diversa rispetto alle comunemente usate tabelle. Molto più a basso livello.

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

A — Atomicità (Atomic). Tutte le modifiche apportate nella transazione vengono registrate, oppure nessuna.

C — Coerenza (Consistency). Dopo il completamento della transazione, lo stato logico del database deve essere internamente consistente. In gran parte questo requisito riguarda il programmatore, ma nel caso dei database SQL riguarda anche le chiavi esterne.

I — Isolamento (Isolation). Le transazioni che vengono eseguite in parallelo non devono influenzarsi reciprocamente.

D — Durable (durata). Dopo il completamento con successo di una transazione, problemi a livelli inferiori (come un'interruzione di corrente) non devono influenzare i dati modificati dalla transazione.

I globali sono strutture di dati non relazionali. Sono stati creati per funzionare in modo estremamente veloce su hardware molto limitato. Esaminiamo l'implementazione delle transazioni nei globali utilizzando l'immagine docker ufficiale di IRIS.

Per supportare le transazioni in IRIS, si utilizzano i comandi: TSTART, TCOMMIT, TROLLBACK.

1. Atomicità

È più facile verificare l'atomicità. Facciamo un controllo dalla console del database.

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

Dopo di che possiamo trarre la conclusione:

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

Otterremo:

1 2 3

Tutto a posto. L'atomicità è stata rispettata: tutte le modifiche sono state salvate.

Complichiamo la situazione, introduciamo un errore e vediamo come si comporta la transazione, se sarà salvata parzialmente o per nulla.

Verifichiamo di nuovo l'atomicità:

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

Dopo di che fermeremo forzatamente il container, lo riavvieremo e vedremo.

docker kill my-iris

Questo comando è praticamente equivalente a un'interruzione dell'alimentazione forzata, poiché invia il segnale di arresto immediato del processo SIGKILL.

Potrebbe essersi salvata parzialmente la transazione?

WRITE ^a(1), ^a(2), ^a(3)
^
 ^a(1)

— No, non è stata salvata.

Proviamo il comando di rollback:

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

WRITE ^a(1), ^a(2), ^a(3)
^
 ^a(1)

Niente è stato salvato nemmeno questa volta.

2. Coerenza

Poiché nelle basi sui globali le chiavi vengono create anche sui globali (ricordo che un globale è una struttura di livello inferiore per l'archiviazione dei dati rispetto a una tabella relazionale), per soddisfare i requisiti di coerenza è necessario includere la modifica della chiave nella stessa transazione della modifica del globale.

Per esempio, abbiamo il globale ^person, in cui archiviamosoggetti e utilizziamo il codice fiscale come chiave.

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

Per avere una ricerca rapida per cognome e nome abbiamo creato la chiave ^index.

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

Per garantire che il database sia coerente dobbiamo aggiungere la personalità in questo modo:

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

Di conseguenza, durante l'eliminazione dobbiamo utilizzare anche una transazione:

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

In altre parole, l'esecuzione della necessità di coerenza ricade interamente sulle spalle del programmatore. Ma quando si tratta di variabili globali, questo è normale a causa della loro natura a basso livello.

3. Isolamento

Qui iniziano le complicazioni. Molti utenti lavorano contemporaneamente sulla stessa base di dati, modificando gli stessi dati.

La situazione è paragonabile a quella in cui molti utenti lavorano contemporaneamente su un unico repository di codice e cercano di eseguire commit di modifiche in molti file allo stesso tempo.

Il database deve gestire tutto in tempo reale. Considerando che nelle aziende importanti esiste anche una persona dedicata al controllo delle versioni (per la fusione dei rami, la risoluzione dei conflitti, ecc.), e che il database deve fare tutto questo in tempo reale, diventa evidente la complessità del compito e l'importanza di progettare correttamente il database e il codice che lo gestisce.

Il database non può comprendere il significato delle azioni degli utenti per evitare conflitti se lavorano sugli stessi dati. Può solo annullare una transazione che contraddice un'altra o eseguirle in sequenza.

Un ulteriore problema è che durante l'esecuzione di una transazione (prima del commit), lo stato del database può essere incoerente, quindi è preferibile che altre transazioni non abbiano accesso a uno stato incoerente del database. Questo obiettivo viene raggiunto nei database relazionali in vari modi: creando snapshot, utilizzando la multiversione delle righe, ecc.

Durante l'esecuzione parallela delle transazioni, è importante che queste non interferiscano l'una con l'altra. Questa è la proprietà di isolamento.

SQL definisce 4 livelli di isolamento:

  • READ UNCOMMITTED
  • READ COMMITTED
  • REPEATABLE READ
  • SERIALIZABLE

Esaminiamo ogni livello singolarmente. I costi di implementazione di ciascun livello aumentano quasi esponenzialmente.

READ UNCOMMITTED — è il livello più basso di isolamento, ma anche il più veloce. Le transazioni possono leggere le modifiche apportate l'una dall'altra.

READ COMMITTED — è il livello successivo di isolamento, che rappresenta un compromesso. Le transazioni non possono leggere le modifiche apportate l'una dall'altra fino al commit, ma possono leggere tutte le modifiche apportate dopo il commit.

Se abbiamo una lunga transazione T1, durante la quale sono avvenuti commit nelle transazioni T2, T3… Tn, che hanno lavorato con gli stessi dati di T1, quando richiediamo dati in T1, riceveremo ogni volta risultati diversi. Questo fenomeno è noto come lettura non ripetibile.

REPEATABLE READ — in questo livello di isolamento non abbiamo il fenomeno della lettura non ripetibile, poiché per ogni richiesta di lettura dei dati viene creato un'immagine dei dati risultanti e, nel riutilizzo durante la stessa transazione, vengono utilizzati i dati dell'immagine. Tuttavia, in questo livello di isolamento è possibile la lettura di dati fantasma. Si intende la lettura di nuove righe che sono state aggiunte da transazioni parallele già confermate.

SERIALIZABLE — il livello più alto di isolamento. È caratterizzato dal fatto che i dati utilizzati in qualche modo nella transazione (lettura o modifica) diventano accessibili ad altre transazioni solo dopo il completamento della prima transazione.

Iniziamo a capire se c'è un isolamento delle operazioni nella transazione rispetto al flusso principale. Apriamo 2 finestre del terminale.

Kill ^t

Write ^t(1)
2

TSTART
Set ^t(1)=2

Non c'è isolamento. Un flusso vede cosa fa l'altro che ha aperto la transazione.

Vediamo se le transazioni di flussi diversi vedono ciò che accade al loro interno.

Apriamo 2 finestre del 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 l'uno dell'altro. Quindi, abbiamo ottenuto il livello di isolamento più semplice, ma anche il più veloce, READ UNCOMMITTED.

In linea di principio, era prevedibile per i globali, per i quali la velocità è sempre stata una priorità.

Cosa fare se abbiamo bisogno di un livello di isolamento più elevato nelle operazioni sui globali?

È importante riflettere su perché siano necessari i livelli di isolamento e come funzionano.

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

Possiamo ottenere questo tramite blocchi adeguati in ObjectScript, che offrono molteplici modi di applicazione: è possibile eseguire blocchi normali, incrementali o multipli con il comando LOCK.

I livelli di isolamento inferiori rappresentano compromessi finalizzati a incrementare la velocità del database.

Vediamo come possiamo ottenere diversi livelli di isolamento tramite i blocchi.

Questo operatore consente di utilizzare non solo i blocchi esclusivi necessari per modificare i dati, ma anche i blocchi condivisi, che possono essere acquisiti in parallelo da più thread quando è necessario leggere dati che non devono essere modificati da altri processi durante la lettura.

Maggiore informazione sul metodo di blocco a due fasi in russo e inglese:

Blocco a due fasi
Two-phase locking

La difficoltà consiste nel fatto che durante la transazione lo stato del database può risultare incoerente, tuttavia questi dati incoerenti sono visibili ad altri processi. Come evitarlo?

Lo faremo attraverso i blocchi creando finestre di visibilità in cui lo stato del database sarà coerente. E tutti gli accessi a tali finestre di visibilità allo stato coerente saranno controllati dai blocchi.

I blocchi condivisi degli stessi dati sono riutilizzabili: possono essere acquisiti da più processi. Questi blocchi impediscono ad altri processi di modificare i dati, cioè vengono utilizzati per formare finestre di stato coerente del database.

I blocchi esclusivi sono utilizzati per le modifiche ai dati — solo un processo può acquisire tale blocco. Un blocco esclusivo può essere acquisito da:

  1. Qualsiasi processo, se i dati sono liberi
  2. Solo il processo che ha un blocco shared sui dati e ha richiesto per primo il blocco esclusivo.

Transazioni nei globali di InterSystems IRIS

Più stretto è il riquadro di visibilità, più a lungo devono aspettare gli altri processi, ma più coerente può essere lo stato del DB in esso.

READ_COMMITED — il punto di questo livello è che vediamo solo i dati confermati dagli altri flussi. Se i dati in un'altra transazione non sono ancora confermati, vediamo la loro vecchia versione.

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

Senza trucchi speciali non saremo in grado di vedere la vecchia versione dei dati in IRIS, quindi dovremo accontentarci dei blocchi.

Di conseguenza, dovremo utilizzare blocchi shared per autorizzare la lettura dei dati solo nei momenti di coerenza.

Supponiamo di avere un database di utenti ^person, che si trasferiscono denaro tra di loro.

Il momento del trasferimento dalla persona 123 alla persona 242:

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

Il momento della richiesta dell'importo di denaro per la persona 123 prima del prelievo deve essere accompagnato da un blocco esclusivo (di default):

LOCK +^person(123)
Scrivi ^person(123)

E se è necessario mostrare lo stato del conto nel pannello personale, si può utilizzare un blocco condiviso o addirittura non utilizzarlo affatto:

LOCK +^person(123)#"S"
Scrivi ^person(123)

Tuttavia, se si suppone che le operazioni di accesso al database vengano eseguite praticamente istantaneamente (ricordo che i globali sono una struttura molto più a basso livello rispetto a una tabella relazionale), allora la necessità di questo livello diminuisce.

REPEATABLE READ — in questo livello di isolamento è consentito che possano esserci più letture dei dati che possono essere modificate da transazioni parallele.

Di conseguenza, dovremo applicare un blocco condiviso per leggere i dati che modifichiamo e blocchi esclusivi sui dati che stiamo cambiando.

Fortunatamente, l'operatore LOCK consente di elencare in dettaglio tutte le blocchi necessari in un'unica istruzione, che possono essere molto numerosi.

LOCK +^person(123, amount)#"S"
lettura ^person(123, amount)

altre operazioni (in questo momento flussi paralleli cercano di modificare ^person(123, amount), ma non possono)

LOCK +^person(123, amount)
modifica ^person(123, amount)
LOCK -^person(123, amount)

lettura ^person(123, amount)
LOCK -^person(123, amount)#”S”

Quando si elencano i lucchetti separandoli con una virgola, vengono presi in sequenza, ma se si fa così:

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

allora vengono presi tutti insieme in modo atomico.

SERIALIZE — dovremo impostare i lucchetti in modo che, alla fine, tutte le transazioni che condividono dati vengano eseguite in sequenza. Per questo approccio, la maggior parte dei lucchetti deve essere esclusiva e presa sulle zone più piccole del globale per le prestazioni.

Se parliamo di prelievi di fondi nel globale ^person, per esso è accettabile solo il livello di isolamento SERIALIZE, poiché i soldi devono essere spesi rigorosamente in sequenza, altrimenti è possibile spendere la stessa somma più volte.

4. Persistenza

Ho effettuato test con l'interruzione brusca del contenitore tramite

docker kill my-iris

Il database ha gestito bene il trasferimento. Non sono stati riscontrati problemi.

Conclusione

Per i globali in InterSystems IRIS, è presente il supporto per le transazioni. Sono realmente atomiche e affidabili. Tuttavia, per garantire la coerenza del database nei globali, è necessaria l'attività del programmatore e l'uso di transazioni, poiché non ci sono complesse strutture incorporate come le chiavi esterne.

Il livello di isolamento nei globali senza l'uso di blocchi è READ UNCOMMITTED, mentre con l'uso di blocchi può essere garantito fino al livello SERIALIZE.

La correttezza e la velocità di esecuzione delle transazioni nei globali dipendono fortemente dalla competenza del programmatore: più ampiamente vengono utilizzati i blocchi shared in lettura, più alto è il livello di isolamento, mentre più ristretti sono i blocchi esclusivi, maggiore è la velocità di esecuzione.

Fonte: habr.com

Acquista un hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista un hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster