Transacciones en las globales de InterSystems IRIS

Transacciones en las globales de InterSystems IRISLa base de datos InterSystems IRIS admite estructuras curiosas para el almacenamiento de datos: las globales. En esencia, son claves multinivel con diversas ventajas adicionales como transacciones, funciones rápidas para navegar por árboles de datos, bloqueos y su propio lenguaje ObjectScript.

Más sobre las globales en la serie de artículos «Las globales: espadas en el almacenamiento de datos»:

Árboles. Parte 1
Árboles. Parte 2
Matrices dispersas. Parte 3

Me intrigó cómo se implementan las transacciones en las globales y qué características tienen. Dado que se trata de una estructura de almacenamiento de datos completamente diferente de las tablas que todos conocemos, es mucho más de bajo nivel.

Como se sabe en la teoría de bases de datos relacionales, una buena implementación de transacciones debe cumplir con los requisitos ACID:

A — Atómico. Se registran todos los cambios realizados en la transacción o ningún cambio en absoluto.

C — Consistencia. Después de completar la transacción, el estado lógico de la base de datos debe ser internamente coherente. En gran medida, este requisito corresponde al programador, pero en el caso de bases de datos SQL, también se refiere a las claves externas.

I — Aislado. Las transacciones que se ejecutan en paralelo no deben influirse entre sí.

D — Durable. Después de que una transacción se complete con éxito, los problemas en niveles inferiores (como un corte de energía, por ejemplo) no deben afectar los datos modificados por la transacción.

Las globales son estructuras de datos no relacionales. Fueron creadas para un funcionamiento extremadamente rápido en hardware muy limitado. Vamos a analizar la implementación de transacciones en las globales usando la imagen oficial de docker de IRIS.

Para soportar transacciones en IRIS, se utilizan los comandos: TSTART, TCOMMIT, TROLLBACK.

1. Atomicidad

Es más fácil verificar la atomicidad. Comprobamos desde la consola de la base de datos.

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

Luego hacemos la salida:

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

Obtenemos:

1 2 3

Todo en orden. Se cumple la atomicidad: todos los cambios se registraron.

Complicaremos la tarea, introduciremos un error y veremos cómo se guarda la transacción, parcialmente o no se guarda en absoluto.

Verificamos de nuevo la atomicidad:

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

Después de lo cual forzaremos el contenedor a detenerse, lo iniciaremos y observaremos.

docker kill my-iris

Este comando es prácticamente equivalente a un apagado forzado, ya que envía la señal de parada inmediata del proceso SIGKILL.

¿Podría ser que la transacción se haya guardado parcialmente?

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

— No, no se guardó.

Probemos el comando de reversión:

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)

Tampoco se guardó nada.

2. Consistencia

Dado que en las bases de datos, los índices se realizan también sobre los globales (recordemos que un global es una estructura de almacenamiento de datos de bajo nivel, más que una tabla relacional), para cumplir con el requisito de consistencia, es necesario incluir el cambio de clave en la misma transacción que el cambio del global.

Por ejemplo, tenemos el global ^person, donde almacenamos identidades, y usamos el NIF como clave.

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

Para tener una búsqueda rápida por apellido y nombre, creamos el índice ^index.

^index('Kamenev', 'Sergey', 1234567) = 1

Para que la base sea consistente, debemos agregar la identidad de esta manera:

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

Por lo tanto, al eliminar también debemos usar una transacción:

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

En otras palabras, el cumplimiento del requisito de consistencia recae completamente en el programador. Pero cuando se trata de globales, esto es normal, debido a su naturaleza de bajo nivel.

3. Aislamiento

Aquí es donde comienzan los problemas. Muchos usuarios trabajan simultáneamente en la misma base, modificando los mismos datos.

La situación es comparable a cuando muchos usuarios trabajan al mismo tiempo con el mismo repositorio de código y tratan de enviar cambios a muchos archivos al mismo tiempo.

La base de datos debe gestionar todo esto en tiempo real. Dado que en empresas serias incluso hay una persona dedicada a controlar las versiones (a fusionar ramas, resolver conflictos, etc.), y la base de datos debe hacerlo todo en tiempo real, se hace evidente la complejidad de la tarea y la necesidad de un diseño correcto de la base de datos y del código que la mantenga.

Una base de datos no puede entender el significado de las acciones realizadas por los usuarios para evitar conflictos cuando trabajan con los mismos datos. Solo puede deshacer una transacción que contradice a otra o ejecutarlas de manera secuencial.

Otro problema es que durante la ejecución de una transacción (antes del commit), el estado de la base de datos puede ser inconsistente. Por lo tanto, es deseable que otras transacciones no tengan acceso a este estado inconsistente, lo que se logra en bases de datos relacionales de diversas maneras: creando instantáneas, mediante la multiversion de filas, etc.

Cuando se ejecutan transacciones en paralelo, es importante que no interfieran entre sí. Esta es la propiedad de aislamiento.

SQL define 4 niveles de aislamiento:

  • LECTURA NO COMPROMETIDA
  • LECTURA COMPROMETIDA
  • LECTURA REPETIBLE
  • SERIALIZABLE

Examinemos cada nivel por separado. Los costos de implementación de cada nivel aumentan casi exponencialmente.

LECTURA NO COMPROMETIDA — es el nivel más bajo de aislamiento, pero también el más rápido. Las transacciones pueden leer los cambios realizados entre ellas.

LECTURA COMPROMETIDA — este es el siguiente nivel de aislamiento, que representa un compromiso. Las transacciones no pueden leer los cambios realizados entre ellas hasta que se comprometan, pero pueden leer cualquier cambio realizado después del commit.

Si tenemos una larga transacción T1, durante la cual se han realizado commits en las transacciones T2, T3 … Tn, que trabajaron con los mismos datos que T1, al consultar datos en T1 recibiremos resultados diferentes cada vez. Este fenómeno se llama lectura no repetible.

LECTURA REPETIBLE — en este nivel de aislamiento no tenemos el fenómeno de lectura no repetible, ya que para cada solicitud de lectura de datos se crea una instantánea del resultado de los datos y cuando se reutilizan en la misma transacción se usan los datos de la instantánea. Sin embargo, en este nivel de aislamiento es posible leer datos fantasma, refiriéndose a la lectura de nuevas filas que fueron añadidas por transacciones paralelas confirmadas.

SERIALIZABLE — el nivel más alto de aislamiento. Se caracteriza por el hecho de que los datos utilizados de alguna manera en la transacción (lectura o modificación) solo se vuelven accesibles a otras transacciones después de completar la primera transacción.

Primero, aclaremos si hay aislamiento entre las operaciones en la transacción y el flujo principal. Abriremos 2 terminales.

Kill ^t

Write ^t(1)
2

TSTART
Set ^t(1)=2

No hay aislamiento. Un hilo ve lo que hace el segundo que abrió la transacción.

Veamos si las transacciones de diferentes hilos ven lo que sucede dentro de ellas.

Abriremos 2 terminales y ejecutaremos 2 transacciones en paralelo.

kill ^t
TSTART
Write ^t(1)
3

TSTART
Set ^t(1)=3

Las transacciones paralelas ven los datos entre sí. Así que hemos obtenido el nivel más simple pero también el más rápido de aislamiento, READ UNCOMMITTED.

En principio, se podría esperar esto para los globales, donde el rendimiento siempre ha sido la prioridad.

¿Qué hacer si necesitamos un nivel de aislamiento más alto en operaciones sobre globales?

Es necesario pensar por qué son necesarios los niveles de aislamiento y cómo funcionan.

El nivel más alto de aislamiento, SERIALIZE, significa que el resultado de las transacciones ejecutadas en paralelo es equivalente a su ejecución secuencial, lo que garantiza la ausencia de colisiones.

Podemos lograr esto mediante bloqueos adecuados en ObjectScript, que tienen una gran variedad de formas de aplicación: se pueden hacer bloqueos ordinarios, incrementales y múltiples usando el comando LOCK.

Los niveles de aislamiento más bajos son compromisos destinados a aumentar la velocidad de la base de datos.

Veamos cómo podemos obtener diferentes niveles de aislamiento mediante bloqueos.

Este operador permite no solo tomar bloqueos exclusivos, necesarios para modificar los datos, sino también bloqueos compartidos que pueden ser utilizados en paralelo por varios hilos, cuando necesitan leer datos que no deben ser modificados por otros procesos durante la lectura.

Más sobre el método de bloqueo en dos fases en ruso e inglés:

→ Bloqueo en dos fases
→ Two-phase locking

La dificultad radica en que, durante la transacción, el estado de la base puede estar desalineado; sin embargo, estos datos desalineados son visibles para otros procesos. ¿Cómo evitar esto?

Lo lograremos a través de bloqueos para crear ventanas de visibilidad en las que el estado de la base estará alineado. Todas las solicitudes a estas ventanas de visibilidad de estado alineado estarán controladas por bloqueos.

Los bloqueos compartidos de los mismos datos son reutilizables: varios procesos pueden obtenerlos. Estos bloqueos impiden que otros procesos modifiquen los datos, es decir, se utilizan para crear ventanas de un estado coherente de la base de datos.

Los bloqueos exclusivos se utilizan para modificar datos; solo un proceso puede adquirir tal bloqueo. Un bloqueo exclusivo puede ser adquirido por:

  1. Cualquier proceso si los datos están libres.
  2. Solo aquel proceso que tiene un bloqueo compartido sobre esos datos y fue el primero en solicitar el bloqueo exclusivo.

Transacciones en las globales de InterSystems IRIS

Cuanto más estrecha sea la ventana de visibilidad, más tiempo tendrán que esperar otros procesos, pero más coherente puede ser el estado de la base de datos en ella.

READ_COMMITED — la esencia de este nivel es que solo vemos los datos confirmados de otros hilos. Si los datos en otra transacción aún no están confirmados, vemos su versión anterior.

Esto nos permite paralelizar el trabajo en lugar de esperar a que se liberen los bloqueos.

Sin trucos especiales, no podremos ver la versión anterior de los datos en IRIS, por lo que tendremos que manejar los bloqueos.

Por lo tanto, deberemos usar bloqueos compartidos para permitir la lectura de datos solo en momentos de coherencia.

Supongamos que tenemos una base de datos de usuarios ^person, que se transfieren dinero entre sí.

El momento de transferencia de la persona 123 a la 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)

El momento de consultar la cantidad de dinero de la persona 123 antes de realizar la deducción debe acompañarse de un bloqueo exclusivo (por defecto):

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

Pero si necesitamos mostrar el estado de la cuenta en el panel personal, se puede utilizar un bloqueo compartido o incluso no usar ningún bloqueo:

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

Sin embargo, si asumimos que las operaciones en la base de datos se realizan prácticamente al instante (recuerde que las estructuras globales son mucho más de bajo nivel que una tabla relacional), la necesidad de este nivel disminuye.

LECTURA REPETIBLE — en este nivel de aislamiento se permite que haya múltiples lecturas de datos que pueden ser modificados por transacciones paralelas.

Por lo tanto, deberemos establecer un bloqueo compartido para la lectura de los datos que modificamos y bloqueos exclusivos para los datos que cambiamos.

La ventaja del operador LOCK permite enumerar en detalle todas las bloqueos necesarios dentro de un solo operador, los cuales pueden ser numerosos.

LOCK +^person(123, amount)#”S”
lectura de ^person(123, amount)

otras operaciones (mientras tanto, flujos paralelos intentan modificar ^person(123, amount), pero no pueden)

LOCK +^person(123, amount)
modificación de ^person(123, amount)
LOCK -^person(123, amount)

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

Al enumerar los bloqueos usando comas, se obtienen secuencialmente, pero si se hace así:

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

se obtienen atómicamente de una vez.

SERIALIZE debemos establecer los bloqueos de tal manera que, en última instancia, todas las transacciones que tienen datos comunes se ejecuten secuencialmente. Para este enfoque, la mayoría de los bloqueos deben ser exclusivos y abarcar las áreas más pequeñas del global para mejorar el rendimiento.

Si hablamos de retiros de fondos en el global ^person, solo se acepta el nivel de aislamiento SERIALIZE, ya que el dinero debe gastarse estrictamente en un orden secuencial, de lo contrario es posible gastar la misma cantidad varias veces.

4. Durabilidad

Realicé pruebas con el apagado abrupto del contenedor mediante

docker kill my-iris

La base lo manejó bien. No se identificaron problemas.

Conclusión

Para los globals en InterSystems IRIS hay soporte para transacciones. Son realmente atómicas y confiables. Sin embargo, para garantizar la coherencia de la base de datos en los globals, se requiere el esfuerzo del programador y el uso de transacciones, ya que no hay complejas construcciones integradas como claves externas.

El nivel de aislamiento en los globals sin usar bloqueos es READ UNCOMMITTED, y al usar bloqueos se puede garantizar hasta el nivel SERIALIZE.

La corrección y velocidad del trabajo de las transacciones en los globals depende en gran medida de la habilidad del programador: cuanto más se utilicen bloqueos compartidos para la lectura, más alto será el nivel de aislamiento, y cuanto más se restrinjan los bloqueos exclusivos, mayor será el rendimiento.

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster