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

Evitando actualizaciones concurrentes del mismo registro

2 comentarios

Como ya hemos visto, hay técnicas para evitar conflictos entre transacciones, esos conflictos ocurren cuando dos o más de ellas quieren actualizar o borrar el mismo registro al mismo tiempo.

Evitando conflictos en las transacciones

Pero a veces no podemos usar esas técnicas porque debemos estar actualizando a los mismos registros concurrentemente, sí o sí. ¿Cómo resolvemos el problema en ese caso?

Bien, primero veamos de manera simplificada cuales son las tareas que realiza el motor del Firebird cuando una transacción READ WRITE READ COMMITTED quiere actualizar un registro:

  1. Lee el registro
  2. Evalúa los datos que se guardarán en el nuevo registro
  3. Escribe el nuevo registro en una página de datos

Recuerda que en Firebird cada vez que se usa el comando UPDATE o el comando DELETE se le está agregando un nuevo registro a la Base de Datos. Si la transacción finaliza con un COMMIT entonces el nuevo registro será el que vale, el que sirve, el que se usará de ahí en adelante y el registro viejo será basura. En cambio, si la transacción finaliza con un ROLLBACK el registro anterior seguirá siendo usado y el nuevo pasará a engrosar la basura. Como ves, en ambos casos se genera basura, y ese es el motivo por el cual periódicamente hay que eliminarla.

¿Y cuál es el problema con esta forma de proceder del Firebird?

Una transacción que se inició con los parámetros anteriores, en el paso 1. lee el último registro confirmado (o sea, que finalizó con un COMMIT) y al llegar al paso 3. vuelve a leerlo para asegurarse que se trata del mismo registro. Si no lo hiciera entonces podría estar silenciosamente sobreescribiendo el trabajo de otra persona.

Si el registro que leyó en el paso 1. es distinto del registro que leyó en el paso 3. entonces lanzará una excepción que nos informará que hay un error en la actualización. Un deadlock. Un bloqueo mortal. La actualización falló.

El tiempo que el Firebird demora en completar los pasos del 1. al 3. es pequeñísimo, se mide en milisegundos, así que usualmente no tendremos problemas con eso … salvo cuando varios usuarios estén actualizando a muchísimos registros al mismo tiempo, pues allí sí tales circunstancias podrían darse.

El Firebird solamente “bloquea” a un registro cuando lo está actualizando, mientras no lo esté actualizando el registro está desbloqueado. Esto significa que si queremos bloquear a un registro, debemos actualizarlo, aunque sea escribiendo en él lo mismo que ya tenía escrito.

Y eso es lo que hace el comando SELECT … WITH LOCK.

Y es ese comando el que deberíamos usar cuando queremos ejecutar un proceso de actualización y queremos tener la seguridad de que no tendremos conflictos con otras transacciones. Esta es una forma de transacción pesimista, la cual aunque no es recomendada en Firebird a veces puede ser necesario usar y por ese motivo es que existe.

El algoritmo entonces sería el siguiente:

SET TRANSACTION 
READ WRITE 
WAIT 
READ COMMITTED 
NO RECORD_VERSION 
LOCK TIMEOUT 5 ;
SELECT MisColumnas FROM MiTabla WHERE MiCondición WITH LOCK ;
UPDATE MiTabla SET MisColumnas = MisNuevosValores WHERE MiCondición ;

¿Qué se hizo aquí?

Primero: se establecieron los parámetros de la transacción:

READ WRITE: La transacción puede leer y también puede escribir en los registros

WAIT: Si otra transacción está actualizando el registro, esta transacción esperará

READ COMMITTED: Leerá la última versión confirmada del registro, sin importar cuando ese registro fue confirmado. Si antes de que esta transacción empezara, o después.

NO RECORD_VERSION: No podrá leer un registro que otra transacción está actualizando

LOCK TIMEOUT 5: Esperará un máximo de 5 segundos hasta que la otra transacción que está actualizando al registro finalice con un COMMIT o con un ROLLBACK. Desde luego que podrías poner más o menos segundos, como te parezca.

Segundo: se consultaron y se bloquearon los registros. Al usar la cláusula WITH LOCK le estamos diciendo que también haga un UPDATE silencioso, para que ninguna otra transacción pueda actualizar a este registro antes de que nosotros lo actualicemos. Fíjate que este segundo paso puede fallar, fallará si ya otra transacción tiene bloqueado a cualquiera de los registros de MiTabla que cumplen con MiCondición.

Tercero: se actualizaron los registros. Si el segundo paso finalizó con éxito entonces este tercer paso también finalizará con éxito. Para eso justamente sirve el SELECT … WITH LOCK, para asegurarnos de que el UPDATE finalizará con éxito.

Observaciones:

Este método es muy efectivo, aunque raramente necesario. En Firebird lo normal y lo correcto es que las transacciones sean optimistas y este es un ejemplo de transacción pesimista. Por ello lo recomendable es que los registros que son bloqueados por el SELECT … WITH LOCK sean muy pocos, idealmente solamente un registro y no más de uno.

La ventaja de este método es que si el SELECT … WITH LOCK finalizó con éxito entonces tendremos una seguridad del 100% de que el UPDATE también finalizará con éxito. Desde luego que para que eso sea verdad, MiCondición debe ser la misma tanto en el SELECT como en el UPDATE. Si ponemos condiciones distintas, nada podremos asegurar.

Conclusión:

En general, y como regla, deberíamos evitar usar los comandos UPDATE y DELETE ya que los conflictos entre transacciones solamente pueden ocurrir cuando se usan esos comandos.

Sin embargo, a veces es necesario usar esos comandos, no podemos evitarlo. En esos casos casi siempre es preferible que nuestras transacciones sean optimistas, porque el Firebird está optimizado para usar transacciones optimistas. Solamente cuando no tenemos alternativa deberíamos usar transacciones pesimistas, y en este artículo se vio una técnica de ello.

Artículos relacionados:

Entendiendo a las transacciones

Modos de bloqueo de las transacciones

Transacciones optimistas y transacciones pesimistas

Algo más sobre transacciones optimistas y transacciones pesimistas

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

Evitando conflictos en las transacciones

Entendiendo las páginas de la Base de Datos

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

Un formulario para mostrar la transacción actual

Deja un comentario

Como conocer los datos de la transacción actual puede ser muy útil cuando estamos en la etapa de crear y depurar aplicaciones hice un pequeño formulario para que muestre esos datos.

Esta es una captura de pantalla del formulario cuando está en ejecución.

FORMULARIO1

Captura 1. Si haces clic en la imagen la verás más grande

Como puedes ver en la Captura 1, toda la información relevante se encuentra ahí. Además, colocando el cursor del mouse sobre cualquiera de los campos se muestra un corto mensaje explicativo, digamos que es un “ayuda memoria”.

Puedes descargar el formulario desde este enlace:

http://www.mediafire.com/download/566t3p6eryburfv/MOSTRAR_TRANSACCION.ZIP

NOTA: Debes reemplazar la llamada a la función SQL_Ejecutar() por una llamada a la función SQLEXEC() porque SQL_Ejecutar() es una función mía que aumenta la potencia de SQLEXEC()

Artículos relacionados:

Una vista para verificar la transacción actual

El índice del blog Firebird21