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