Respuestas a más preguntas sobre transacciones (4)

1 comentario

¿Estás respondiendo las preguntas sobre transacciones?

Si lo estás haciendo, felicitaciones, si no lo estás haciendo, …. deberías, para aprender más.

Veamos cuales son las respuestas correctas del artículo: Más preguntas sobre transacciones (4)

Pregunta 1. Si una transacción solamente hará SELECTs ¿cuál debe ser su acceso?

[READ ONLY]     [READ WRITE]     [Es indiferente]

La respuesta correcta: Es indiferente. ¿Por qué? porque en ambos casos podrá hacer los SELECTs, sin embargo es preferible que sea READ ONLY porque las transacciones READ ONLY terminan más rápido que las transacciones READ WRITE debido a que el Firebird realiza menos tareas, pero no es obligatorio que sea READ ONLY.

Pregunta 2. Si una transacción solamente hará INSERTs ¿cuál debe ser su acceso?

[READ ONLY]     [READ WRITE]     [Es indiferente]

En este caso la transacción debe ser READ WRITE porque no se puede realizar un INSERT (ni un UPDATE, ni un DELETE) en las transacciones READ ONLY.

Pregunta 3. Si una transacción hará SELECTs e INSERTs, ¿cuál debe ser su acceso?

[READ ONLY]     [READ WRITE]     [Es indiferente]

Debe ser READ WRITE sí o sí, porque no se puede realizar un INSERT (ni un UPDATE, ni un DELETE) en una transacción READ ONLY.

Pregunta 4. Si una transacción quiere conocer lo que otras transacciones que finalizaron con COMMIT hicieron. ¿Cuál debe ser su aislamiento?

[READ COMMITTED]     [SNAPSHOT]     [Es imposible, nunca puede conocer lo que otras transacciones hicieron]

El aislamiento READ COMMITTED permite conocer lo que otras transacciones que ya finalizaron con un COMMIT hicieron. El aislamiento SNAPSHOT no permite tal cosa.

Pregunta 5. Si una transacción T1 ha bloqueado a una fila y una transacción T2 quiere actualizar a esa misma fila. ¿Cuál debe ser el modo de bloqueo de la transacción T2 para que pueda hacerlo?

[WAIT]     [NO WAIT]     [Cualquiera, es indiferente]     [Nunca la transacción T2 podrá actualizar esa fila]

Debe ser WAIT y entonces la transacción T2 esperará hasta que la transacción T1 finalice. Si el modo de bloqueo de la transacción T2 es NO WAIT entonces inmediatamente obtendrá una excepción y nunca podrá actualizar a esa fila.

Pregunta 6. Si una transacción T1 ha bloqueado a una fila, y quieres que la transacción T2 pueda ver la última versión confirmada de esa fila. ¿Cuál versión de registro debe tener la transacción T2?

[RECORD_VERSION]     [NO RECORD_VERSION]     [Es indiferente]

Debes usar RECORD_VERSION, porque en este caso la transacción T2 podrá ver la última versión confirmada de la fila. Si usas NO RECORD_VERSION entonces la fila que actualizó la transacción T1 será inaccesible para la transacción T2, inclusive para lectura.

Pregunta 7. Si una transacción T1 ha bloqueado una fila y no quieres que la transacción T2 pueda ni siquiera ver la última versión confirmada de esa fila ¿Cuál versión de registro debe tener la transacción T2?

[RECORD_VERSION]     [NO RECORD_VERSION]     [Es indiferente]

Debe ser NO RECORD_VERSION porque en este caso si una fila está bloqueada por una transacción las demás transacciones no pueden acceder a esa fila, ni siquiera para lectura.

Pregunta 8. Si una transacción quiere tener acceso exclusivo a todas las tablas que utiliza ¿puede hacerlo?

[Sí]     [No]

La respuesta es . En ese caso su aislamiento debe ser SNAPSHOT TABLE STABILITY. Si la transacción pudo iniciarse (no siempre podrá, otras transacciones podrían impedírselo) entonces tendrá acceso exclusivo.

Pregunta 9. Si una transacción quiere tener acceso exclusivo solamente a algunas de las tablas que utiliza ¿puede hacerlo?

[Sí]     [No]

La respuesta es . En ese caso debe escribir RESERVING y la lista de las tablas a las que quiere acceder de forma exclusiva.

Artículos relacionados:

Entendiendo a las transacciones

3 preguntas sobre transacciones

Respuestas a las 3 preguntas sobre transacciones

Más preguntas sobre transacciones (2)

Respuestas a más preguntas sobre transacciones (2)

Más preguntas sobre transacciones (3)

Respuestas a más preguntas sobre transacciones (3)

Más preguntas sobre transacciones (4)

El índice del blog Firebird21

El foro del blog Firebird21

Anuncios

Más preguntas sobre transacciones (4)

4 comentarios

Parece que muchos tienen miedo de responder a las preguntas que plantea este blog ¿por qué será? Son sólo preguntas, y si se responde incorrectamente cuando se conoce la respuesta correcta nunca más se la olvida.

En ajedrez hay un viejo dicho: “se aprende más de una partida perdida que de cien partidas ganadas”

¿Por qué?

Porque si te interesa ser bueno en ese juego, cuando pierdes una partida, la miras y la analizas una y otra vez, para descubrir tus errores y corregirlos y nunca más volver a cometerlos. En cambio si ganaste, ni te molestas en reproducir la partida ¿para qué? la ganaste y a otra cosa.

Análogamente si quieres ser bueno en Firebird deberías responder a todas las preguntas planteadas. ¿Te equivocaste en algunas? Eso no es malo, porque cuando leas las respuestas correctas ya no las olvidarás.

Bueno, luego de esa larga introducción, continuemos:

Pregunta 1. Si una transacción solamente hará SELECTs ¿cuál debe ser su acceso?

[READ ONLY]     [READ WRITE]     [Es indiferente]

Pregunta 2. Si una transacción solamente hará INSERTs ¿cuál debe ser su acceso?

[READ ONLY]     [READ WRITE]     [Es indiferente]

Pregunta 3. Si una transacción hará SELECTs e INSERTs, ¿cuál debe ser su acceso?

[READ ONLY]     [READ WRITE]     [Es indiferente]

Pregunta 4. Si una transacción quiere conocer lo que otras transacciones que finalizaron con COMMIT hicieron. ¿Cuál debe ser su aislamiento?

[READ COMMITTED]     [SNAPSHOT]     [Es imposible, nunca puede conocer lo que otras transacciones hicieron]

Pregunta 5. Si una transacción T1 ha bloqueado a una fila y una transacción T2 quiere actualizar a esa misma fila. ¿Cuál debe ser el modo de bloqueo de la transacción T2 para que pueda hacerlo?

[WAIT]     [NO WAIT]     [Cualquiera, es indiferente]     [Nunca la transacción T2 podrá actualizar esa fila]

Pregunta 6. Si una transacción T1 ha bloqueado a una fila, y quieres que la transacción T2 pueda ver la última versión confirmada de esa fila. ¿Cuál versión de registro debe tener la transacción T2?

[RECORD_VERSION]     [NO RECORD_VERSION]     [Es indiferente]

Pregunta 7. Si una transacción T1 ha bloqueado una fila y no quieres que la transacción T2 pueda ni siquiera ver la última versión confirmada de esa fila ¿Cuál versión de registro debe tener la transacción T2?

[RECORD_VERSION]     [NO RECORD_VERSION]     [Es indiferente]

Pregunta 8. Si una transacción quiere tener acceso exclusivo a todas las tablas que utiliza ¿puede hacerlo?

[Sí]     [No]

Pregunta 9. Si una transacción quiere tener acceso exclusivo solamente a algunas de las tablas que utiliza ¿puede hacerlo?

[Sí]     [No]

Artículos relacionados:

Entendiendo a las transacciones

3 preguntas sobre transacciones

Respuestas a las 3 preguntas sobre transacciones

Más preguntas sobre transacciones (2)

Respuestas a más preguntas sobre transacciones (2)

Más preguntas sobre transacciones (3)

Respuestas a más preguntas sobre transacciones (3)

El índice del blog Firebird21

El foro del blog Firebird21

¿Por qué en Firebird es preferible que las transacciones sean optimistas?

4 comentarios

Para entenderlo, debes tener bien en claro cuando se puede actualizar (UPDATE) o borrar (DELETE) una fila en Firebird.

En Firebird, todo lo que hagamos estará siempre dentro de una transacción, siempre, sin excepción. Todas las operaciones (INSERT, UPDATE, DELETE, SELECT, FETCH) siempre se encuentran dentro de una transacción.

Y una transacción puede actualizar (UPDATE) o borrar (DELETE) una fila solamente si la tiene bloqueada. Si una transacción no ha bloqueado a una fila entonces no podrá actualizarla ni borrarla, le será imposible hacerlo.

En un determinado momento solamente puede haber una transacción bloqueando a una fila. Es imposible que dos o más transacciones tengan a una fila bloqueada al mismo tiempo. No se puede. Solamente una transacción la puede estar bloqueando.

Y solamente la transacción que la tiene bloqueada la puede actualizar (UPDATE) o borrar (DELETE), las demás transacciones no podrán hacerlo hasta que la transacción que la tiene bloqueada finalice (con un COMMIT o con un ROLLBACK, pero debe finalizar antes de que esa fila pueda ser actualizada o borrada por otra transacción).

Por lo tanto, lo que ocurre es lo siguiente:

  • Se bloquea la fila
  • Se la actualiza (con el comando UPDATE) o se la borra (con el comando DELETE)
  • Finaliza la transacción

Entonces, la pregunta es ¿en qué momento se bloquea la fila?

La transacción optimista piensa: “Cuando necesite actualizar la fila la encontraré desbloqueada y entonces yo podré bloquearla”

La transacción pesimista piensa: “No sé si cuando necesite actualizar la fila la encontraré desbloqueada por eso lo mejor es que la bloquee ahora mismo para que cuando necesite actualizarla ya la tenga bloqueada”.

Ya ves por que a una se le llama “optimista” y a la otra se le llama “pesimista”.

¿Cómo actúa una transacción optimista?

  1. Inicia la transacción
  2. Realiza ochopotecientas tareas
  3. Bloquea la fila
  4. Actualiza la fila
  5. Realiza ochopotecientas tareas más
  6. Finaliza la transacción

¿Cómo actúa una transacción pesimista?

  1. Inicia la transacción
  2. Bloquea la fila
  3. Realiza ochoptecientas tareas
  4. Actualiza la fila
  5. Realiza ochopotecientas tareas más
  6. Finaliza la transacción

¿Cuál es la diferencia?

Que la transacción optimista bloquea a la fila inmediatamente antes de actualizarla, un microsegundo antes; en cambio la transacción pesimista la bloquea mucho antes de utilizarla. Como puedes ver, los pasos 2. y 3. están invertidos, los demás son iguales.

¿Y esto qué implica?

Que las transacciones pesimistas mantienen a la fila bloqueada durante mucho más tiempo, porque realizar esas “ochopotecientas tareas” puede demorar bastante.

Pero además hay otro problema con las transacciones pesimistas y es que pueden estar bloqueando a muchas filas que jamás usarán. Si una transacción pesimista bloquea a una tabla está bloqueando a todas las filas de esa tabla, aunque solamente necesite actualizar algunas. Si una transacción pesimista bloquea a una tabla que tiene 2500 filas aunque solamente necesite actualizar 200 de esas filas, ninguna de las 2500 filas podrán ser actualizadas por otras transacciones, no solamente las 200 filas que necesita estarán bloqueadas, las 2500 filas estarán bloqueadas.

¿Qué ocurre si una transacción quiere bloquear una fila que ya está bloqueada por otra transacción?

Que no podrá bloquearla. Eso porque en un determinado momento una fila puede estar bloqueada por una sola transacción, no pueden dos transacciones bloquear a la misma fila en el mismo momento. El intento de bloquear a una fila que está bloqueada por otra transacción se conoce como conflicto o colisión. La transacción que intentó bloquear a la fila y falló tiene dos alternativas:

a) Espera hasta que la transacción que tiene a la fila bloqueada finalice con un COMMIT o con un ROLLBACK, para después de eso volver a intentar bloquear dicha fila. Eso ocurre cuando el modo de bloqueo de la transacción es WAIT (esperar)

b) Sale inmediatamente, con un mensaje de error. Eso ocurre cuando el modo de bloqueo de la transacción es NO WAIT (no esperar)

¿Cuáles son las ventajas de usar transacciones optimistas?

  1. Las filas quedan bloqueadas durante muy poco tiempo
  2. La probabilidad de que haya conflictos (colisiones) con otras transacciones es muy pequeña

¿Cuáles son las ventajas de usar una transacción pesimista?

En Firebird es extremadamente raro necesitar usar transacciones pesimistas, en general casi todo puede hacerse con transacciones optimistas. Si requieres de muchas transacciones pesimistas entonces es muy probable que aún no entiendas como funciona Firebird. Pero también usarlas tiene algunas ventajas, ellas son:

  1. Rápidamente, se puede numerar secuencialmente y sin que haya números faltantes y sin provocar deadlocks
  2. Se pueden ejecutar procesos que requieren que otras transacciones no estén insertando, actualizando, o borrando filas

Resumiendo:

En Firebird todo se hace dentro de una transacción, ninguna operación (INSERT, UPDATE, DELETE, SELECT, FETCH) puede hacerse afuera de una transacción. Las transacciones pueden ser optimistas o pesimistas. Las operaciones de UPDATE y de DELETE requieren que la fila sea bloqueada antes de ser actualizada o borrada, no se puede actualizar ni borrar una fila que previamente no fue bloqueada. Las transacciones optimistas bloquean a la fila durante muy poco tiempo, las transacciones pesimistas bloquean a la fila durante más tiempo. Cuanto menos tiempo una fila esté bloqueada mucho mejor, porque eso reduce el riesgo de colisiones. Ocurre una “colisión” cuando una transacción quiere actualizar o borrar una fila que otra transacción tiene bloqueada. Cuando ocurre una colisión la transacción que no pudo bloquear a la fila solamente tiene dos posibilidades: a) espera hasta que la transacción que tiene a la fila bloqueada termine, o b) sale inmediatamente con un mensaje de error.

En aplicaciones multi-usuario es común y es normal que existan colisiones porque dos o más transacciones a veces querrán actualizar la misma fila al mismo tiempo. Eso a veces es inevitable. Pero un buen profesional siempre tratará de que esas colisiones ocurran la menor cantidad de veces posibles y por eso en Firebird hay que tratar de usar siempre transacciones optimistas, porque son las que durante menor tiempo tienen a las filas bloqueadas.

Artículos relacionados:

Entendiendo a las transacciones

Transacciones optimistas y transacciones pesimistas

Algo más sobre transacciones optimistas y transacciones pesimistas

El índice del blog Firebird21

El foro del blog Firebird21

 

Algo más sobre transacciones optimistas y transacciones pesimistas

2 comentarios

En Firebird todo lo que hagamos ocurrirá dentro de una transacción y esas transacciones pueden ser de dos tipos: optimistas y pesimistas.

  • La ventaja de las transacciones optimistas es que terminan más rápido que las transacciones pesimistas equivalentes y que ocasionan muchos menos conflictos (o colisiones)
  • La ventaja de las transacciones pesimistas es que nos permiten numerar sin tener huecos (o sea: números faltantes) y que podemos ejecutar procesos que requieren acceso exclusivo a tablas.

Si una sola persona se encarga de insertar, de actualizar y de borrar las filas de una tabla entonces jamás tendremos problemas, y por lo tanto podríamos usar transacciones optimistas en el 100% de los casos.

Pero si dos o más personas insertan, actualizan o borran filas de una misma tabla entonces allí sí pueden surgir problemas. Y cuantas más personas puedan hacerlo, mayor será la probabilidad de que ocurran problemas.

Este tema ya fue tratado en el artículo:

Transacciones optimistas y transacciones pesimistas

pero aquí lo trataremos con un poco más de profundidad.

En Firebird lo normal, lo correcto, lo recomendable es tener (casi) siempre transacciones optimistas. Una excepción es cuando necesitamos numerar en forma secuencial y consecutiva. Otra excepción es cuando necesitamos ejecutar un proceso de actualización que no debe ser “molestado” por otra transacción.

Caso 1. Si podemos numerar: 1, 2, 15, 32, 39, … entonces deberíamos usar una transacción optimista. Esos números los obtendremos con la ayuda de un generador (también se le llama secuencia). También usaríamos transacciones optimistas en casi todos los INSERT, UPDATE, DELETE, SELECT, FETCH, que hagamos.

Caso 2. Si la numeración debe ser: 1, 2, 3, 4, 5, 6, … y no puede faltar ningún número entonces nuestra transacción deberá ser pesimista.

Mostrando el problema

Supongamos que tenemos una tabla de ALUMNOS y necesitamos que cada uno de esos alumnos tenga un CÓDIGO que debe ser secuencial y consecutivo. El CÓDIGO del último alumno registrado es el 617 y por lo tanto el siguiente CÓDIGO deberá ser el 618.

¿Qué puede pasar si nuestra transacción es optimista?

La transacción T1 hace un SELECT para hallar el código del último alumno, encuentra que es 617, le suma 1 y obtiene 618. Después hace un INSERT y un COMMIT para insertarle una fila a la tabla de ALUMNOS, la cual tendrá el número 618 en la columna CÓDIGO.

El problema es que si la transacción T2 antes del COMMIT de la transacción T1 también hizo un SELECT a la tabla de ALUMNOS también encontrará que el último código es el 617, también le sumará 1, y también obtendrá 618 y también querrá insertar una fila a la tabla de ALUMNOS que en su columna CÓDIGO tenga el número 618.

Como ya existe un CÓDIGO con el número 618 (porque lo grabó la transacción T1) entonces el intento de COMMIT de la transacción T2 será rechazado (o al menos debería ser rechazado, porque los códigos no deberían estar duplicados y por lo tanto tendría que haber una Unique Key sobre la columna CÓDIGO).

Esta situación le ha creado un conflicto a la transacción T2. No puede insertar el número de CÓDIGO que ella legítimamente halló (o sea, el número 618) porque la transacción T1 también lo había hallado e hizo un COMMIT.

Entonces, lo único que le queda por hacer a la transacción T2 es terminar con un ROLLBACK, o sea sin penas ni gloria. ¿Qué pasó aquí? que la transacción T2 halló que el CÓDIGO debía ser 618, hizo un INSERT, hizo un COMMIT, su COMMIT falló porque ya existía un CÓDIGO con el número 618 y entonces debe terminar con un ROLLBACK.

Pero el usuario que ejecutó la transacción T2 no está conforme, él quiere que se inserten los datos del nuevo alumno así que inicia otra transacción para conseguir su objetivo. Pero entre el ROLLBACK y el inicio de su nueva transacción ocurrió un lapso de tiempo durante el cual otro usuario inició la transacción T3. Y entonces la transacción T4 (heredera de la transacción T2) tendrá un nuevo conflicto, porque hallará que el CÓDIGO del nuevo alumno debe ser 619 y cuando quiera guardarlo encontrará que no puede porque ese número ya fue utilizado por la transacción T3.

En un entorno de muchos usuarios concurrentes algo así podría ocurrir frecuentemente y ser harto frustrante para los usuarios. Podrías mostrarles un mensaje diciendo algo como: “La grabación falló, vuelve a intentarlo”, pero después de unas cuantas veces la mayoría de los usuarios estarán hartos.

Haciendo que una transacción sea pesimista

Como veremos a continuación, la solución es que la transacción sea pesimista, entonces la pregunta es: ¿cómo conseguimos que una transacción sea pesimista?

Bien, esto lo podemos hacer de dos formas:

  1. Con SET TRANSACTION
  2. Con el comando UPDATE

Para que la transacción sea pesimista con SET TRANSACTION escribimos algo como:

SET TRANSACTION
   [SNAPSHOT TABLE STABILITY]
   [RESERVING MiTabla1, MiTabla2, MiTabla3, ...
   FOR [SHARED | PROTECTED] {READ | WRITE}]

Si usamos el aislamiento SNAPSHOT TABLE STABILITY entonces tendremos acceso exclusivo a cada tabla que la transacción utilice. Si esas tablas están listadas después de la cláusula RESERVING entonces serán bloqueadas en el mismo momento en que la transacción empiece y liberadas cuando la transacción finalice. Esta es la forma más restrictiva de todas y por lo tanto la menos recomendable en Firebird.

Si no usamos el aislamiento SNAPSHOT TABLE STABILITY y especificamos la cláusula RESERVING entonces las tablas MiTabla1, MiTabla2, MiTabla3, etc. estarán reservadas para ser usadas por esta transacción. Eso no significa que otras transacciones no podrán usar esas tablas sino que no podrán impedir que esta transacción las utilice. Cuidado con eso porque es un error de concepto muy común, mucha gente cree que al reservar una tabla las demás transacciones no podrán usarlas, eso es falso. Lo que se hace al reservar una tabla es asegurarse de que ninguna otra transacción impedirá que esta transacción tenga acceso a esa tabla.

Ejemplo: Si la transacción T1 reservó a la tabla VENTAS entonces la transacción T2 no le podrá impedir a la transacción T1 acceder a la tabla VENTAS.

Las cuatro combinaciones posibles son:

SHARED READ. Permite a cualquier transacción leer datos y a cualquier transacción que tiene el modo de acceso WRITE actualizar filas. Es la menos restrictiva.

SHARED WRITE. Permite a cualquier transacción con modo de acceso WRITE y cuyo aislamiento sea SNAPSHOT o READ COMMITTED leer y actualizar filas. Las transacciones con modo de acceso READ y cuyo aislamiento sea SNAPSHOT o READ COMMITTED podrán leer filas, pero no actualizarlas.

PROTECTED WRITE. Impide que las otras transacciones puedan actualizar filas, las transacciones SNAPSHOT y READ COMMITED podrán leer filas, pero solamente esta transacción podrá actualizar filas.

PROTECTED READ. Ninguna transacción podrá actualizar filas, ni siquiera esta transacción, pero todas las transacciones podrán leer filas.

Desde luego que la primera transacción que reservó una tabla es la que tiene preferencia. Por ejemplo si la transacción T1 reservó a una tabla para PROTECTED READ y la transacción T2 quiere reservar a esa misma tabla para PROTECTED WRITE entonces la transacción T2 no podrá iniciar, será rechazada.

 Para que la transacción sea pesimista con UPDATE hacemos lo siguiente:

  • Hacemos un UPDATE a una fila de la tabla MiTabla1
  • Realizamos operaciones de INSERT, UPDATE, DELETE, SELECT, FETCH en otras tablas (y también en MiTabla1, si queremos)
  • Todas las transacciones que pueden entrar en conflicto con esta transacción también deben comenzar con un UPDATE a la tabla MiTabla1

¿Cuándo iniciar una transacción pesimista con SET TRANSACTION?

En general usamos SET TRANSACTION PROTECTED WRITE cuando queremos ejecutar un stored procedure que no debe ser “molestado” por otras transacciones.

Veamos lo que podría ocurrir si nuestra transacción no es PROTECTED WRITE: queremos guardar en una tabla el saldo actual de todos nuestros clientes (una fila por cada cliente, desde luego) y para eso al saldo inicial de cada cliente le sumamos todas las ventas que le hicimos a crédito y le restamos todas las cobranzas. Pero ¿y si mientras estamos realizando ese proceso alguien guarda una venta a crédito o una cobranza del cliente que estamos procesando? entonces el saldo que hallemos podría ser incorrecto. Desde luego que algo así podría ocurrir y sería desastroso. No deberíamos permitir que ocurra un error tan catastrófico. Para evitarlo nuestra transacción debe ser PROTECTED WRITE, de esa manera solamente nuestra transacción podrá actualizar las tablas MiTabla1, MiTabla2, MiTabla3, etc. Las demás transacciones podrán leer el contenido de esas tablas, pero no cambiar ese contenido.

Fíjate que usamos PROTECTED WRITE para hacer a la transacción pesimista porque las filas que queremos actualizar son muchas, si queremos actualizar una sola fila entonces lo correcto es usar UPDATE para hacer a la transacción pesimista.

¿Cuándo iniciar una transacción pesimista con UPDATE?

Cuando queremos hallar el siguiente número y no queremos que haya huecos (o sea, números faltantes) lo correcto es usar una transacción pesimista con UPDATE.

En el caso de la tabla de ALUMNOS que vimos más arriba el proceso a realizar sería el siguiente:

Abrimos una transacción como READ COMMITTED y WAIT.

En una tabla AUXILIAR tenemos guardado el código del último alumno. En este ejemplo sería el número 617.

Hacemos un UPDATE a esa fila de la tabla AUXILIAR, escribiendo algo como: UPDATE AUXILIAR SET ULTIMO_CODIGO =ULTIMO_CODIGO + 1, por lo tanto en ULTIMO_CODIGO tendremos 618.

Hacemos un SELECT a la tabla AUXILIAR para conocer el valor de ULTIMO_CODIGO, escribiendo algo como: SELECT ULTIMO_CODIGO FROM AUXILIAR, y obtendremos 618.

Le hacemos un INSERT a la tabla de ALUMNOS, poniendo en su columna CODIGO el número que se encuentra en la columna ULTIMO_CODIGO de la tabla AUXILIAR, es decir, el número 618

Terminamos la transacción

¿Qué ocurrirá?

Que la transacción T1 actualizó una fila de la tabla AUXILIAR y por lo tanto esa fila quedará bloqueada hasta que la transacción T1 finalice (con un COMMIT o con un ROLLBACK).

En la tabla ALUMNOS se guardó el CÓDIGO que le corresponde al nuevo alumno, en este caso el 618

La transacción T2 (que debe ser READ COMMITTED y WAIT) también quiso insertarle una fila a la tabla ALUMNOS pero primero debe actualizar una fila de la tabla AUXILIAR. No podrá hacerlo hasta que la transacción T1 finalice. Después de finalizar la transacción T1 recién entonces la transacción T2 podrá actualizar la fila de la tabla AUXILIAR, le pondrá el valor 619 y ese será el CÓDIGO que guardará en la tabla ALUMNOS.

Como puedes ver, la solución es muy sencilla. La transacción T2 debe ser READ COMMITTED para que pueda leer el valor actualizado de la columna ULTIMO_CODIGO después que la transacción T1 finalice. Es cierto que hay una espera de la transacción T2 porque deberá esperar hasta que la transacción T1 finalice, pero eso es todo. Desde luego que eso implica que el modo de bloqueo de las transacciones debe ser WAIT, es decir que la transacción T2 debe quedarse esperando hasta que la transacción T1 finalice, ya que si el modo de bloqueo es NO WAIT la transacción T2 terminará inmediatamente cuando no pueda actualizar la fila de la tabla AUXILIAR. Y no es eso lo que queremos.

Lo que queremos es que si la transacción T2 no puede actualizar una fila de la tabla AUXILIAR se quede esperando hasta que pueda actualizarla.

Resumiendo:

Las transacciones en Firebird pueden ser optimistas o pesimistas. Lo normal, lo más frecuente, lo más recomendable, es que las transacciones sean optimistas, hay muy pocos casos en que se necesitan transacciones pesimistas. Si usas muchas transacciones pesimistas entonces aún no sabes usar a Firebird correctamente. Hay dos formas de conseguir que una transacción sea pesimista: a) usando SET TRANSACTION, b) usando UPDATE. En general se la hace pesimista con SET TRANSACTION cuando las filas a procesar son muchas porque cuando se reserva una tabla se reservan todas las filas de esa tabla. Y se usa UPDATE cuando las filas son muy pocas (lo más frecuente es que sea una sola fila). Si queremos numerar en forma consecutiva y sin que falten números entonces debemos usar una transacción pesimista con UPDATE, cuyo aislamiento sea READ COMMITTED y el modo de bloqueo sea WAIT.

Artículos relacionados:

Entendiendo a las transacciones

Transacciones optimistas y transacciones pesimistas

El índice del blog Firebird21

El foro del blog Firebird21

Entendiendo a las transacciones

7 comentarios

Entender correctamente como funcionan las transacciones es importantísimo en Firebird porque todo lo haces dentro de una transacción, no existe alguna operación que pueda realizarse afuera de ellas. Eso significa que siempre todos tus INSERT, UPDATE, DELETE, FETCH, SELECT y EXECUTE PROCEDURE se ejecutan dentro de una transacción, sí o sí.

¿Por qué eso?

Porque de esta manera existe la seguridad 100% de que tu Base de Datos cumpla con ACID, que es un paradigma que todos los buenos SGBDR (y por lo tanto todas sus bases de datos) deben cumplir.

Las bases de datos que cumplen con ACID son totalmente confiables y todas las bases de datos de Firebird cumplen con ACID al 100%, sin excepción.

https://firebird21.wordpress.com/2013/05/10/entendiendo-acid/

Iniciando una transacción

Si no había una transacción abierta entonces una transacción se inicia automáticamente cuando escribes un comando (por ejemplo: INSERT, UPDATE, DELETE, SELECT) .

El comando SET TRANSACTION (que verás más abajo) no inicia la transacción, lo que hace es:

  1. Un COMMIT a la transacción actual (el cual, como todo COMMIT, puede finalizar exitosamente o fallar)
  2. Establecer los parámetros que serán usados por la siguiente transacción

Finalizando una transacción

Hay solamente dos formas (normales, porque también hay formas anormales) en que una transacción puede ser finalizada:

  • Con un COMMIT
  • Con un ROLLBACK

Si finaliza con un COMMIT entonces todo lo que se hizo dentro de la transacción queda guardado en las tablas de la Base de Datos.

Si finaliza con un ROLLBACK entonces todo lo que se hizo dentro de la transacción es desechado, eliminado, como si nunca se hubiera hecho.

Un COMMIT puede fallar, un ROLLBACK jamás falla.

En los ejemplos que hay más abajo puedes ver casos de COMMITs que fallan.

Sintaxis de SET TRANSACTION:

SET TRANSACTION
   [NAME NombreTransacción]
[READ WRITE | READ ONLY]
[ [ISOLATION LEVEL] { SNAPSHOT [TABLE STABILITY]
| READ COMMITTED [[NO] RECORD_VERSION] } ]
[WAIT | NO WAIT]
[LOCK TIMEOUT segundos]
[NO AUTO UNDO]
[IGNORE LIMBO]
[RESERVING | USING ]

NAME se usa cuando abres una transacción dentro de tu programa. Te sirve para asignarle un nombre a la transacción. De esa manera podrías tener varias transacciones abiertas al mismo tiempo y realizar operaciones independientes en cada una de ellas. Es opcional, si no quieres no lo usas pero si no lo usas entonces solamente puedes tener una transacción abierta.

Con READ WRITE le pides al Firebird que abra la transacción para lectura y para escritura. Es el parámetro por defecto. Y es el que debes utilizar si dentro de la transacción tendrás los comandos INSERT o UPDATE o DELETE. Con READ ONLY le pides al Firebird que abra la transacción para lectura solamente. Y es el que debes utilizar cuando dentro de tu transacción solamente tendrás el comando SELECT o el comando EXECUTE PROCEDURE y éste no realiza ningún INSERT ni UPDATE ni DELETE. Usar READ ONLY en una transacción que no modifica la Base de Datos hace que ésta finalice más rápido y eso es algo muy bueno.

ISOLATION LEVEL es el nivel de aislamiento de la transacción y puede tener uno de estos valores:

SNAPSHOT. Significa que la transacción solamente conoce los valores que fueron confirmados (se confirma con un COMMIT) antes de que ella empezara. En general se usa este nivel de aislamiento cuando la transacción solamente tendrá SELECTs.

SNAPSHOT TABLE STABILITY. Obtiene acceso exclusivo para escritura en todas las tablas involucradas y también en todas las tablas relacionadas con las anteriores mediante una Foreign Key. Ninguna otra transacción podrá modificar (insertar, actualizar, borrar) las tablas que una transacción SNAPSHOT TABLE STABILITY está usando. Y una transacción SNAPSHOT TABLE STABILITY no podrá iniciar si alguna de las tablas que necesita está siendo usada por otra transacción READ WRITE. En Firebird no es recomendable usar este aislamiento. Es muy peligroso porque impide que otras transacciones modifiquen las tablas. En el rarísimo caso de que debas usar este aislamiento trata de que la transacción termine muy rápido o de que se ejecute cuando nadie más usa la Base de Datos.

READ COMMITED. Permite que la transacción pueda “ver” todas las filas que fueron insertadas, actualizadas o borradas por otras transacciones y que fueron confirmadas (mediante un COMMIT) por esas otras transacciones. Es el aislamiento que normalmente se usa en los programas que insertan, modifican o borran filas de las tablas. Este aislamiento además puede ser RECORD_VERSION o NO RECORD_VERSION.

RECORD VERSION. En este caso la transacción puede leer todas las filas que fueron confirmadas por otras transacciones. Y si la transacción es READ WRITE entonces podrá modificar también esas filas pero siempre y cuando haya empezado después que las otras. Ejemplo:

      • En la tabla PRODUCTOS el precio de venta de “Televisor Toshiba de 20 pulgadas” es de 120 dólares
      • Empezó la transacción T1
      • Empezó la transacción T2
      • La transacción T1 cambió el precio de venta de “Televisor Toshiba de 20 pulgadas” a 100 dólares
      • La transacción T2 ve que el precio de venta de “Televisor Toshiba de 20 pulgadas” es de 120 dólares. Como la transacción T1 aún no finalizó con un COMMIT, la transacción T2 ve el precio que ese producto tenía cuando empezó la transacción T2. La transacción T2 no puede saber lo que hace la transacción T1 antes de que la transacción T1 finalice con un COMMIT.
      • La transacción T1 finalizó con un COMMIT
      • La transacción T2 cambió el precio de venta de “Televisor Toshiba de 20 pulgadas” a 110 dólares
      • La transacción T2 finalizó con un COMMIT
      • Ahora, el precio de venta es de 110 dólares porque es el que corresponde al último COMMIT

Como la transacción T2 había empezado después que la transacción T1 entonces el COMMIT de la transacción T2 fue exitoso. Si hubiera empezado antes, hubiera sido rechazado. Por ejemplo:

      • En la tabla PRODUCTOS el precio de venta de “Televisor Toshiba de 20 pulgadas” es de 120 dólares
      • Empezó la transacción T1
      • Empezó la transacción T2
      • La transacción T2 cambió el precio de venta de “Televisor Toshiba de 20 pulgadas” a 110 dólares
      • La transacción T1 ve que el precio de venta de “Televisor Toshiba de 20 pulgadas” es de 120 dólares. Como la transacción T2 aún no finalizó con un COMMIT, la transacción T1 ve el precio que ese producto tenía cuando empezó la transacción T1.  La transacción T1 no puede saber lo que hace la transacción T2 antes de que la transacción T2 finalice con un COMMIT.
      • La transacción T2 finalizó con un COMMIT
      • La transacción T1 cambió el precio de venta de “Televisor Toshiba de 20 pulgadas” a 100 dólares
      • La transacción T1 intenta finalizar con un COMMIT
      • Su intento de COMMIT es rechazado. Como la transacción T1 empezó antes que la transacción T2 entonces la transacción T1 no puede modificar una fila que fue modificada por la transacción T2, ya que la transacción T2 es más nueva
      • Ahora, el precio de venta es de 110 dólares, que corresponde al último COMMIT exitoso

NO RECORD_VERSION. Es el valor por defecto. Impide que la transacción pueda siquiera leer una fila que fue actualizada por otra transacción y que aún no fue confirmada. Por ejemplo:

      • En la tabla PRODUCTOS el precio de venta de “Televisor Toshiba de 20 pulgadas” es de 120 dólares
      • Empezó la transacción T1
      • Empezó la transacción T2
      • La transacción T1 cambió el precio de venta de “Televisor Toshiba de 20 pulgadas” a 100 dólares
      • La transacción T2 no puede ver cual es el precio de venta de “Televisor Toshiba de 20 pulgadas” porque la transacción T1 aún no finalizó con un COMMIT. Por lo tanto la transacción T2 no tiene acceso a esa fila, ni siquiera para lectura. Y ahora pueden ocurrir dos cosas:
        1. Si el modo de bloqueo de la transacción T2 es WAIT entonces la transacción T2 esperará hasta que la transacción T1 finalice con un COMMIT o con un ROLLBACK. En ambos casos, como la transacción T2 es más nueva que la transacción T1 entonces la transacción T2 podrá finalizar con un COMMIT
        2. Si el modo de bloqueo de la transacción T2 es NO WAIT entonces la transacción T2 recibirá una notificación de que la fila que quiso modificar está bloqueada
      • Si la transacción T1 finaliza con un COMMIT, entonces el precio de venta será de 100 dólares, si finaliza con un ROLLBACK el precio de venta continuará en 120 dólares

WAIT. Este es el modo de bloqueo por defecto. Sirve para lo siguiente: cuando una transacción no puede modificar o borrar una fila porque otra transacción está usando esa fila, espera hasta que la otra transacción termine. Si la transacción que estaba esperando es más nueva entonces podrá modificar a esa fila cuando sea desbloqueada. Si es más vieja, obtendrá inmediatamente una notificación de error. No hay que usar WAIT en programas que muchos usuarios utilizan para modificar o borrar las mismas filas porque puede causar que muchos usuarios estén esperando varios minutos antes de poder continuar con sus tareas. Tampoco tiene sentido usar WAIT en transacciones SNAPSHOT sin usar también LOCK TIMEOUT en ellas. De todas maneras hay excepciones y lo que puedes hacer es verificar si en tu caso es bueno usar o no usar WAIT. En general, si son pocos los usuarios que modifican las mismas filas y las transacciones son cortas, usar WAIT suele ser lo correcto. Cuando el modo de bloqueo es NOWAIT el Servidor del Firebird inmediatamente informa que ocurrió un conflicto porque una transacción intenta modificar una fila que otra transacción también está modificando. Si muchos usuarios están modificando las mismas filas NOWAIT les informará rápidamente que ocurrió un problema mientras que WAIT les hará esperar. En transacciones SNAPSHOT lo recomendable suele ser usar NOWAIT y cuando se encuentra un conflicto hacer un ROLLBACK o esperar unos segundos y reintentar la modificación.

LOCK TIMEOUT es muy útil para evitar que una transacción espere indefinidamente que la fila sea desbloqueada. Solamente se puede usar cuando el modo de bloqueo es WAIT. Por ejemplo: WAIT LOCK TIMEOUT 5 esperará un máximo de 5 segundos para que la fila sea desbloqueada. Si no fue desbloqueada entonces se recibirá una notificación, como si el modo de bloqueo hubiera sido NOWAIT. Lastimosamente no puede usarse en transacciones iniciadas dentro de un programa.

NO AUTO UNDO le indica al Servidor del Firebird que no guarde los datos que normalmente guarda para hacer el ROLLBACK de la transacción. Si tu transacción tendrá muchos INSERT y estás seguro que no finalizará con un ROLLBACK entonces con esta opción conseguirás que la transacción finalice más rápidamente.

IGNORE LIMBO. Con esta opción los registros creados por las transacciones limbo son ignorados. Una transacción está en limbo si la segunda etapa de un COMMIT de dos etapas falla. Los COMMIT de dos etapas se usan en transacciones que involucran a dos bases de datos. No se usa esta opción en transacciones que son iniciadas dentro de los programas.

RESERVING reserva todas las tablas cuyos nombres se encuentran a continuación. La reserva empieza cuando la transacción empieza y termina cuando la transacción finaliza. Eso le garantiza a la transacción tener acceso a todas esas tablas y ninguna otra transacción podrá impedirle el acceso a las tablas listadas. SNAPSHOT TABLE STABILITY bloquea a todas las tablas que están dentro de la transacción (si hay 7 tablas bloquea a las 7), en cambio RESERVING solamente reserva a las tablas cuyos nombres se especificaron (quizás de esas 7 solamente 2 necesitaban realmente ser reservadas), siendo por lo tanto menos restrictivo y el método preferible cuando se deben bloquear o reservar tablas (algo que en Firebird muy raramente debería hacerse). USING limita los nombres de las bases de datos a los cuales la transacción puede acceder a los que aquí se hayan especificado.

Opciones por defecto

Las opciones por defecto de las transacciones (o sea, las que usará el Servidor del Firebird si no le indicas otra cosa) son:

READ WRITE + WAIT + SNAPSHOT

Lecturas sucias

Firebird no permite las lecturas sucias. Se le llama lectura sucia a los cambios que hace una transacción y antes de que dicha transacción termine con un COMMIT desde otra transacción se pueden ver esos cambios. Por ejemplo: la transacción T1 está haciendo un INSERT y antes de que la transacción T1 finalice con un COMMIT desde la transacción T2 ya se está viendo ese INSERT que hizo la transacción T1. Otros SGBDR permiten esa situación, pero Firebird no. Y es lo correcto porque eso otorga mucha mayor seguridad.

NOTA:

Todas las transacciones siempre pueden ver a todos los registros que fueron confirmados (finalizaron con un COMMITantes de que ellas empezaran. Pero solamente las transacciones READ COMMITED pueden ver a los registros que fueron confirmados después de que ellas empezaran.

Artículos relacionados:

COMMIT y ROLLBACK en stored procedures y triggers

Detectando aplicaciones y usuarios que mantienen las transacciones abiertas durante mucho tiempo

Terminar las transacciones de los SELECTs

Modos de bloqueo de las transacciones

Bloqueos mortales

Entendiendo ACID

La arquitectura MGA

Transacciones optimistas y transacciones pesimistas

El índice del blog Firebird21

El foro del blog Firebird21