¿Cómo funciona una transacción SNAPSHOT?

Deja un comentario

Como recordarás, una transacción en Firebird puede tener uno de estos tres aislamientos:

  • READ COMMITED
  • SNAPSHOT
  • SNAPSHOT TABLE STABILITY

Los aislamientos le dicen al Firebird lo que debe hacer cuando una fila quiere ser actualizada (UPDATE) o borrada (DELETE) por más de una transacción al mismo tiempo.

Veamos un ejemplo:

  1. La transacción T1 empieza (su aislamiento es SNAPSHOT, y su modo de bloqueo es WAIT)
  2. La transacción T2 empieza (su aislamiento es SNAPSHOT)
  3. La transacción T2 actualiza (UPDATE) a una fila X de la tabla PRODUCTOS.
  4. La transacción T2 finaliza con un COMMIT
  5. La transacción T3 empieza (su aislamiento es SNAPSHOT)
  6. La transacción T3 también actualiza (UPDATE) a la misma fila X de la tabla PRODUCTOS
  7. La transacción T1 trata de actualizar (UPDATE) a la misma fila X de la tabla PRODUCTOS, pero como esa fila está bloqueada por la transacción T3 entonces deberá esperar hasta que la transacción T3 finalice.

Sin embargo, debemos notar que en este ejemplo la transacción T1 fallará siempre. ¿Por qué? Porque tendrá un conflicto con la transacción T3 si la transacción T3 finalizó con un COMMIT, o tendrá un conflicto con la transacción T2 si la transacción T3 finalizó con un ROLLBACK. O sea que, sin importar como termine la transacción T3 (con un COMMIT o con un ROLLBACK) la transacción T1 fallará.

Te puedes preguntar: ¿y por qué la transacción T1 debe esperar hasta que finalice la transacción T3? Después de todo, en ambos casos fallará, entonces ¿por qué la espera?

La respuesta está en que el Firebird solamente verifica la última versión de una fila para saber si ocurrió un conflicto o no. Si verificara la anteúltima versión entonces podría hacer fallar a la transacción T1 en el mismo momento en que hiciera un UPDATE, pero eso implicaría más trabajo y por lo tanto solamente verifica a la última versión.

Una transacción SNAPSHOT en el momento en que se inicia copia en su porción de la memoria RAM de la computadora la TIP (Transaction Inventory Page) conteniendo a todas las transacciones que están activas en ese momento. O sea, la transacción T2 conoce cuales son todas las transacciones que estaban activas cuando se inició la transacción T2, pero desconoce totalmente a las transacciones que se iniciaron después que ella y por lo tanto supone que están activas ya que evidentemente no habían finalizado cuando empezó la transacción T2. Cada transacción tiene un número único, que se guarda en la TIP. Cuando una transacción inicia bloquea a su propio número y lo desbloquea cuando finaliza (sea con un COMMIT o con un ROLLBACK). De esta manera es muy fácil saber si una transacción está activa o no. Si no se puede desbloquear su número, está activa. Si se puede desbloquear su número, no está activa.

Cuando una transacción SNAPSHOT trata de bloquear a una fila para hacerle un UPDATE o un DELETE, lo que puede ocurrir es lo siguiente:

  • Si la última versión de esa fila fue creada por una transacción que tiene un número menor y que no está en su copia de la TIP, el bloqueo tendrá éxito. ¿Por qué? porque como no está en su copia de la TIP y el número es menor, significa que una transacción anterior que insertó o actualizó a la fila ya finalizó con un COMMIT.
  • Si la última versión de una fila fue creada por una transacción cuyo número está en la TIP entonces debe verificar si esa transacción ya finalizó. ¿Y cómo lo verifica? Tratando de bloquear el número que esa otra transacción tiene en la TIP. Si lo consigue, la otra transacción ya finalizó y se puede bloquear a la fila con éxito.
  • Si el último COMMIT a la fila fue realizado por una transacción que tiene un número de transacción mayor, eso significa que esa otra transacción empezó después. Y por lo tanto, no se podrá bloquear a la fila.

Ejemplo:

Empieza una transacción, su número es 529, y en su copia de la TIP tiene a los números 521, 525, 526, 528. Eso significa que esas 4 transacciones están activas, aún no han finalizado ni con un COMMIT ni con un ROLLBACK.

La transacción cuyo número es 529 quiere hacer un UPDATE a una fila X de la tabla PRODUCTOS, el número de transacción que tiene la última versión de esa fila X es el 291. Como el número de transacción 291 no está en la copia de la TIP, eso significa que la transacción 291 (o una transacción anterior a ella) ya ha finalizado con un COMMIT y por lo tanto se podrá realizar el UPDATE con éxito a la fila X.

La transacción cuyo número es 529 quiere hacer un UPDATE a una fila X de la tabla PRODUCTOS, el número de transacción que tiene la última versión de esa fila es el 526. Como el número de transacción 526 está en la copia de la TIP, eso significa que la transacción 526 estaba activa cuando se inició la transacción 529. Pero ¿está activa ahora? quizás sí, quizás no, para verificarlo la transacción 529 trata de bloquear al número 526 en la TIP global, no en su propia copia de la TIP. Si consigue realizar el bloqueo, la transacción 526 ya no está activa y entonces podrá realizar el UPDATE con éxito. ¿Y si no consigue bloquear, qué hace? Eso dependerá del modo de bloqueo. Si es WAIT, seguirá intentando bloquear hasta tener éxito. Si es NO WAIT lanzará una excepción con un mensaje de error.

La transacción cuyo número es 529 quiere hacer un UPDATE a una fila X de la tabla PRODUCTOS, el número de transacción que tiene la última versión de esa fila es el 540. ¿Podrá la transacción 529 realizar el UPDATE? Depende. Si la transacción 540 finaliza con un COMMIT, no podrá. ¿Por qué no? Porque 540 es mayor que 529. Si la transacción 540 finaliza con un ROLLBACK entonces hay que buscar el número que tiene la última versión de esa fila X cuya transacción finalizó con un COMMIT. Si el último COMMIT a la fila X fue realizado por la transacción 520, la transacción 529 podrá realizar el UPDATE (porque 520 es menor que 529). Si el último COMMIT a la fila X fue realizado por la transacción 535, la transacción 529 no podrá realizar el UPDATE (porque 535 es mayor que 529).

Una transacción SNAPSHOT solamente puede actualizar (UPDATE) o borrar (DELETE) a las filas creadas por las transacciones que empezaron antes que ella.

Recuerda que el Firebird crea una nueva versión de una fila cada vez que se ejecuta el comando UPDATE o el comando DELETE en esa fila.

Sin importar como finalice la transacción (con un COMMIT o con un ROLLBACK) hay una nueva fila. Esto va creando filas inservibles (se les llama «basura») y por ese motivo hay que limpiar a la Base de Datos de basura cada cierto tiempo.

Una fila tiene la siguiente forma:

| Nº de Transacción | Columna1 | Columna2 | Columna 3| etc.

Importante: Una transacción T1 (cuyo aislamiento es SNAPSHOT) puede actualizar (UPDATE) o borrar (DELETE) a una fila solamente cuando el Nº de Transacción que realizó el último COMMIT a esa fila es menor que el número de la transacción T1.

Artículos relacionados:

Entendiendo a las transacciones

Entendiendo a los identificadores de las transacciones

Modos de bloqueo de las transacciones

Bloqueos mortales

Lock conflict on no wait transaction. Deadlock

El índice del blog Firebird21

El foro del blog Firebird21

¿Cuál es la mejor manera de almacenar una IP?

4 comentarios

Este artículo es una colaboración de Esteban Herrero, uno de los más antiguos seguidores de este blog. Gracias Esteban.

 

Si tenemos la siguiente IP 192.168.10.21 y quisiera tener un campo para almacenarla, lo más fácil sería crear un campo Char(15), pero podríamos pensar que no todas las IP tienen 15 caracteres, entonces sería mejor utilizar un VarChar(15). Lo cierto es que cada carácter almacenado ocupa  1 byte, haciendo que el almacenamiento de esta Ip lleve 13 bytes o 15 con todos los caracteres.

Pero hay una mejor forma de guardar una Ip y es utilizando un campo BigInt y sólo estaríamos utilizando 64 Bits (8 bytes). Xq no usar un campo Integer, la razón es muy simple es 32 bits y el valor máximo es 2,147,483,647 y nuestro número convertido no entraría.

 

Vamos a un ejemplo:

 

Ip 192.168.10.21

 

11000000 10101000 00001010 00010101 (en binario)

 

Ahora a Convertir:

21 * 1  +

10 * 256 +

168 * 256 * 256 +

192 * 256 * 256 * 256 = 3,232,238,101

Cómo hacerlo en Firebird? Con estos 2 procedimientos:

Execute Procedure SP_INTTOIP(3232238101);

Execute Procedure sp_iptoint(‘192.168.10.21’);

 

CREATE PROCEDURE SP_INTTOIP(

NIP BIGINT)

RETURNS(

IP VARCHAR(15))

AS

DECLARE VARIABLE Octet1 BIGINT;

DECLARE VARIABLE Octet2 BIGINT;

DECLARE VARIABLE Octet3 BIGINT;

DECLARE VARIABLE Octet4 BIGINT;

DECLARE VARIABLE RestoIP BIGINT;

BEGIN

Octet1 = :nIp / 16777216;

RestoIp = :nIp – (Octet1 * 16777216);

Octet2 = RestoIp / 65536;

RestoIp = RestoIp – (Octet2 * 65536);

Octet3 = RestoIp / 256;

Octet4 = RestoIp – (Octet3 * 256);

Ip = Cast(Octet1 as VARCHAR(3)) || ‘.’ || Cast(Octet2 as VARCHAR(3)) || ‘.’ || Cast(Octet3 as VARCHAR(3)) || ‘.’ || Cast(Octet4 as VARCHAR(3));

SUSPEND;

END;

 

CREATE PROCEDURE SP_IPTOINT(

CIP VARCHAR(15))

RETURNS(

NINT BIGINT)

AS

DECLARE VARIABLE lcColumna VARCHAR(4);

DECLARE VARIABLE nCan1 SMALLINT;

DECLARE VARIABLE nFactor BIGINT;

BEGIN

 

nInt = 0;

nFactor = 16777216;

 

WHILE ((Char_Length(cIp) > 0) AND (POSITION(‘.’ in CIP) <> 0)) DO BEGIN

EXECUTE PROCEDURE Parser(cIp, ‘.’) RETURNING_VALUES :lcColumna;

nCan1 = CHAR_LENGTH(:lcColumna) + 1;

cIp = Overlay(cIp placing » from 1 for nCan1);

cIp = trim(cIp);

nInt = nInt + (Cast(:lcColumna as Smallint) * nFactor);

nFactor = nFactor / 256;

END

nInt = nInt + CAST(cIp as SMALLINT);

SUSPEND;

END;

 

CREATE PROCEDURE PARSER(

TCTEXTO VARCHAR(18192),

TCSEPARADOR VARCHAR(12))

RETURNS(

FTCNOMBRE VARCHAR(1024))

AS

DECLARE VARIABLE lnPosicion SMALLINT;

BEGIN

lnPosicion = Position(tcSeparador IN tcTexto);

ftcNombre = Left(tcTexto, lnPosicion – 1) ;

END;