¿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

¿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

Lock conflict on no wait transaction. Deadlock.

2 comentarios

Si al querer hacerle un UPDATE o un DELETE a una tabla recibes este mensaje de error ¿qué significa?

Que la transacción T1 hizo un UPDATE o un DELETE y antes de que la transacción T1 terminara (con un COMMIT o con un ROLLBACK) la transacción T2 también intentó hacerle un UPDATE o un DELETE a la misma tabla. Eso provocó un conflicto. Y como el modo de bloqueo de la transacción T2 es NO WAIT entonces cuando ocurre un conflicto inmediatamente recibe el mensaje de error correspondiente.

LOCK CONFLICT

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

¿Y cuál es la solución?

Eso depende de las circunstancias.

  • Si la operación que quiso hacer la transacción T2 (es decir, el UPDATE o el DELETE) puede esperar entonces se termina la transacción T2 (con un ROLLBACK) y más tarde se inicia una transacción T3 para que realice la operación fallida.
  • Si la operación que quiso hacer la transacción T2 no puede esperar (quizás porque la transacción T1 jamás podría finalizar normalmente con un COMMIT o con un ROLLBACK debido a que dicha transacción quedó “colgada” porque un usuario presionó las teclas CTRL+ALT+DEL o algo similar) entonces hay que eliminar a la transacción problemática. Este es el caso más complicado.

Eliminando la transacción que causa el bloqueo

En la Captura 1. podemos ver que hay un conflicto y la causa de ese conflicto es que la transacción 133608 está bloqueando los UPDATE y los DELETE a las filas de una tabla. El programa que había iniciado a la transacción 133608 fue cerrado abruptamente por el “Administrador de tareas” del Windows y es por lo tanto imposible que alguna vez la transacción 133608 finalice con un COMMIT o con un ROLLBACK, como debería ser.

¿Y entonces?

Entonces debemos eliminar a la transacción problemática (la número 133608, en este ejemplo) de forma manual. Eso lo conseguiremos con el siguiente comando:

DELETE FROM
   MON$TRANSACTIONS
WHERE
   MON$TRANSACTION_ID = 133608;

COMMIT;

La tabla MON$TRANSACTIONS es una tabla de monitoreo, interna del Firebird, o sea que siempre existe en todas las Bases de Datos. En esa tabla se guardan los datos de cada transacción. Si se elimina una fila de esa tabla entonces la transacción correspondiente deja de existir.

Por supuesto, no te olvides de terminar tu transacción eliminadora con un COMMIT.

¿Pero y si falla el DELETE?

Hay ocasiones en las que el DELETE a una fila de la tabla MON$TRANSACTIONS te fallará. ¿Qué puedes hacer entonces? Finalizar a la transacción T2 con un ROLLBACK. Si eso no es suficiente, salir de todos los programas que usan a esa Base de Datos. Y si sigue sin ser suficiente entonces detener el Servidor del Firebird y luego reiniciarlo (algo que no siempre es posible porque los demás usuarios se quejarán hasta en chino).

Pero … cuidado, porque esto no termina aquí. La transacción problemática (la 133608 en este ejemplo) dejó basura dentro de la Base de Datos y esa basura deberás eliminarla alguna vez (haciendo un sweep). No hagas el sweep cuando mucha gente está usando la Base de Datos porque el sweep siempre causa que todas las operaciones se vuelvan muy lentas.

Artículos relacionados:

Bloqueos mortales

Modos de bloqueo de las transacciones

Entendiendo sweep y garbage collection

El índice del blog Firebird21

 

Actualización de saldos: cuando sí y cuando no

8 comentarios

Es muy común que los programadores quieran tener en una columna el saldo actualizado de ese ítem, pero … ¿es eso conveniente?

Supongamos que tenemos una tabla de PRODUCTOS con las columnas:

  • Identificador del producto
  • Código del producto
  • Nombre del producto
  • Cantidad inicial
  • Cantidad actual

Luego, cada vez que entra o sale un producto del stock se actualiza la columna “Cantidad actual” para así tener siempre a mano cual es la cantidad actual de dicho producto.

¿Es ésa una buena práctica?

No.

¿Por qué no?

Porque cada vez que una transacción actualiza una columna se bloquea la fila completa. La primera transacción que intente hacerlo no tendrá problemas pero las siguientes sí, porque la fila que quieren actualizar está bloqueada. Y en ese caso solamente tienen dos alternativas:

  1. Esperar que la transacción que tiene bloqueada a una fila la desbloquee
  2. Abandonar el intento de actualización con un mensaje de error

Y ninguna de esas alternativas es aceptable en un entorno donde hay mucha concurrencia (o sea, muchas computadoras queriendo actualizar el mismo ítem)

Ejemplo del problema que puede ocurrir:

Un supermercado tiene 25 cajas, es fin de año, época de muchas ventas. La caja 1 registra un producto, actualizando su cantidad en stock. Las cajas 7 y 18 quisieron actualizar también la cantidad en stock de ese producto y fueron rechazadas. La caja 1 liberó al producto, el cual ahora fue actualizado por la caja 7 pero la caja 18 continúa esperando. La caja 1 registra otro producto, y al hacerlo lo bloquea. Las cajas 12, 14, y 20 que querían también registrar ese producto no pueden hacerlo. La caja 3 bloqueó a otro producto, que la caja 5 también debe actualizar y por lo tanto no puede.

Puede ser un problema interminable porque cuando una caja libera a un producto otra caja lo bloquea.

Por lo tanto, querer tener actualizada la cantidad en stock de un producto que puede ser vendido desde muchas computadoras y al mismo tiempo, es un gran error.

A la gente no le gusta esperar, si tienen que estar esperando y esperando y esperando hasta que cada producto pueda ser registrado en la caja lo más probable es que se vayan y no vuelvan. Y si se van se pierden clientes, y si se pierden clientes se pierden ventas, y si se pierden ventas se pierde dinero. Y si pierden dinero, lo más probable es que los dueños de la empresa se deshagan rápidamente del programa inútil y del programador inútil, y contraten algo que realmente les sirva.

¿En qué casos sí se puede actualizar el saldo en línea?

Cuando la probabilidad de que el ítem sea accedido para actualización desde 2 ó más computadoras es cero o casi cero. Por ejemplo, en el momento en que se le vende a un cliente o se le cobra a un cliente sí es correcto actualizar su saldo. ¿Por qué? Porque es altamente improbable o imposible que al mismo cliente se le esté vendiendo o cobrando desde dos o más computadoras. Si el cliente está en la caja 5 pues está en la caja 5, no está también en la caja 12 y en la caja 18. Por lo tanto, es imposible que haya conflicto cuando se quiere actualizar su saldo.

Lo mismo sucedería con los proveedores. Es muy raro que un proveedor entregue varias facturas y esas facturas sean cargadas al mismo tiempo y en distintas computadoras; entonces actualizar su saldo cada vez que se carga una factura es correcto.

Algo similar sucede con los alumnos y las cobranzas de las cuotas. Es prácticamente imposible que al mismo alumno se le esté cobrando al mismo tiempo desde dos o más computadoras.

¿Cómo se obtiene el saldo actual si no se lo actualiza en línea?

Como vimos, en el caso de los productos de un supermercado sería un error querer tener actualizados los saldos de los productos en línea (o sea, en el mismo momento en que se venden). Pero los usuarios necesitan conocer ese saldo, ¿cómo lo solucionamos?

Hay dos alternativas:

  1. Si la velocidad con la cual se recuperan los datos de la consulta es alta, un simple SELECT será suficiente. A la cantidad inicial del producto (que tenemos guardada en la tabla PRODUCTOS) se le suman todas las entradas y se le restan todas las salidas, hallándose así la cantidad actual.
  2. Si realizar la consulta especificada en el punto 1. demora mucho, entonces se tiene una tabla de SALDOS que son actualizados periódicamente (una vez al día, una vez a la semana, una vez al mes, etc.) y luego a la cantidad que hay en esa tabla de SALDOS se le suman las entradas y se le restan las salidas. Por ejemplo, si la tabla de SALDOS se actualiza diariamente (por ejemplo: al final de la jornada laboral), para saber la cantidad actual de un producto se obtiene la cantidad que tenía ayer (ese dato está en la tabla de SALDOS) y se le suman todas las entradas y se le restan todas las salidas que ocurrieron el día de hoy.

Conclusión:

Cuando un ítem (por ejemplo: un producto) puede tener mucha concurrencia (o sea, que accedan a sus datos para modificarlos desde varias computadoras y al mismo tiempo) no se debe actualizar el saldo de ese ítem porque si se lo actualiza solamente acarreará problemas a los usuarios.

Cuando un ítem (por ejemplo: un proveedor) tiene baja concurrencia entonces sí es correcto actualizar su saldo.

La pregunta que debemos hacernos es: ¿es probable que al mismo tiempo se quiera actualizar el saldo de este ítem desde 2 ó más computadoras? Si la respuesta es afirmativa entonces no debemos actualizar el saldo.

Artículos relacionados:

Modos de bloqueo de las transacciones

Bloques mortales

El índice del blog Firebird21

Transacciones optimistas y transacciones pesimistas

2 comentarios

Este artículo está basado en el documento: “Transaçoes em Firebird” de Eugénio Reis.

Conceptos generales

  1. Normalmente las únicas operaciones que pueden bloquear un registro son las operaciones de escritura. En Firebird existe una excepción a esa regla: un SELECT crea un bloqueo conocido como shared lock (bloqueo compartido) el cual permite las lecturas y las escrituras al registro pero impide que la tabla sea borrada con un DROP. Una tabla puede ser borrada con el comando DROP solamente si ninguno de sus registros tiene un shared block, o sea si nadie está haciendo un SELECT a dicha tabla.
  2. La gran diferencia entre el tratamiento optimista y el tratamiento pesimista es la cantidad de tiempo durante el cual el registro queda bloqueado. En el optimista el tiempo es menor.
  3. Las alteraciones son registradas en un área aparte llamada log de transacciones
  4. El bloqueo pesimista tiende a saturar el uso del log porque fuerza a la Base de Datos a mantener las transacciones abiertas por mucho tiempo
  5. En el Firebird no se pueden tener lecturas sucias. Una lectura es sucia cuando la transacción T2 puede ver las alteraciones que está realizando la transacción T1 antes de que la transacción T1 realice el COMMIT correspondiente.
  6. Un registro jamás puede ser alterado al mismo tiempo por dos transacciones concurrentes

¿Es mejor el tratamiento optimista o el tratamiento pesimista?

En general, es preferible el optimista porque el pesimista tiende a arruinar el rendimiento de la Base de Datos, a crear situaciones de deadlock (bloqueos mortales) con más frecuencia, a generar filas de espera por la disponibilidad de los datos y a aumentar la competición entre los usuarios.

Cuanto mayor sea la cantidad de usuarios mayores serán los trastornos que el tratamiento pesimista causará porque a mayor cantidad de bloqueos, mayor cantidad de problemas.

Estudio de caso 1: control del stock

Si un cliente llega a la caja con el producto en la mano, no debe ser validado para saber si hay existencia de ese producto. La prueba de que hay está en las manos del cliente. El cajero inclusive debe ser capaz de realizar la venta aunque la Base de Datos esté off-line y más adelante cuando esté on-line se corre un proceso de actualización.

Es también común hacer un recuento físico del stock cada seis meses o un año para evitar que las discrepancias inevitables causadas por factores como hurto, pérdida, deterioro, etc., se vuelvan muy grandes.

Si un cliente llama por teléfono para preguntar si hay disponibilidad de un producto porque quiere comprarlo el vendedor no debería bloquear el registro sino hacer una reserva del mismo. Y aunque en la computadora él vea que hay existencia de ese producto podría ser falso porque hubo hurto.

Estudio de caso 2: cambio en la ficha del cliente

En general, sería muy raro que los datos de un cliente deban ser modificados por dos usuarios al mismo tiempo. Un caso posible es cuando el cliente utiliza una tarjeta de crédito y el usuario legítimo y quien le robó los datos de su tarjeta de crédito están realizando compras y pagando con esa tarjeta. Para estos casos una medida de seguridad es que entre un pago con tarjeta y el siguiente transcurra cierto tiempo. Claro que quien pagó primero será el aceptado.

Estudio de caso 3: reservas

Ocurre cuando el cliente desea reservar algo: un asiento en un avión o en un ómnibus, un auto que desea alquilar, una habitación en un hotel, una mesa en un restaurante, una entrada para un concierto, un producto específico, etc.

El producto específico puede ser el más ilustrativo porque posiblemente no existan dos productos exactamente iguales (“quiero comprar el cuadro titulado ‘Las princesas’ del pintor Joaquín Velázquez”. En este caso es evidente que el vendedor tiene un solo cuadro para vender)

El problema es que si se está vendiendo por Internet dos o más personas pueden estar queriendo comprar el mismo cuadro al mismo tiempo y quieren colocarlo en su “carrito de compras”.

Si la primera persona lo colocó en su carrito de compras ¿la segunda persona no puede hacerlo? Supongamos que se le da a la primera persona un tiempo prudencial para decidirse, ya que podría desistir de la compra, de 20 minutos. ¿Le hacemos esperar 20 minutos a la segunda persona para avisarle que el producto está disponible? Lo más probable es que la segunda persona desista y si la primera persona también lo hizo, entonces perdimos la venta.

Un tratamiento pesimista nos haría perder ventas (como en el ejemplo anterior) y también afectaría muy mal nuestra reputación como vendedores.

Estudio de caso 4: numeración secuencial

En los tres casos anteriores lo correcto es el tratamiento optimista, ahora veremos un caso en que el adecuado es el tratamiento pesimista.

Las situaciones en las cuales se necesitan números secuenciales que no pueden tener huecos entre ellos requieren de un tratamiento pesimista.

Ejemplos típicos son: numeración de las matrículas de los alumnos, números de patentes de los vehículos, Facturas de venta, numeración serial de productos, etc.

Esto se puede hacer de dos maneras:

  1. Recorriendo la tabla hasta encontrar el último número utilizado
  2. Teniendo una tabla auxiliar

Lo correcto, por velocidad y confiabilidad, es usar el método 2.

Entonces, nuestra tabla auxiliar tendría tres columnas:

  • AUX_IDENTI, identificador de la fila (generalmente SMALLINT es más que suficiente)
  • AUX_CODIGO, código de la secuencia (por ejemplo: “MAT”, “CUO”, “ASU”, “001-001-“)
  • AUX_ULTNUM, último número usado (normalmente SMALLINT aunque a veces se necesitaría INTEGER)

En líneas generales el algoritmo es el siguiente:

  1. Se abre la transacción
  2. UPDATE AUXILIAR SET AUX_ULTNUM = AUX_ULTNUM + 1 WHERE AUX_CODIGO = ‘MAT’. Este UPDATE bloqueará el registro inmediatamente y cualquier otro programa en la red que desee bloquear este mismo registro tendrá que esperar
  3. lnUltimoNumero = (SELECT AUX_ULTNUM FROM AUXILIAR WHERE AUX_CODIGO = ‘MAT’). Este SELECT es necesario para conocer cual es el valor actual de la columna AUX_ULTNUM
  4. Se graba en el registro y la columna adecuados de la tabla principal el valor de lnUltimoNumero
  5. Se realiza el COMMIT. Si tiene éxito, el valor de lnUltimoNumero estará grabado en la tabla auxiliar y en la tabla principal. Si falla, se hace un ROLLBACK y quedarán con sus valores anteriores. En ambos casos la transacción fue cerrada y todos los bloqueos liberados. Los demás procesos pueden generar nuevos números secuenciales a partir de ese momento.

Lo más crítico en este proceso es el tiempo durante el cual el registro de la tabla auxiliar queda bloqueado. Recuerda que si ese tiempo se prolonga causará trastornos a los demás usuarios. Los objetivos del tratamiento pesimista son conseguir seguridad y consistencia, pero debe conseguirse en transacciones lo más cortas posibles para no afectar negativamente a los demás usuarios.

Un punto importante cuando el tratamiento es pesimista es el orden en el cual se ejecutan el UPDATE y el SELECT. Deben realizarse siempre en el mismo orden: o siempre primero el UPDATE o siempre primero el SELECT. No importa cual de ellos se realice primero sino que en todos los procesos se mantenga el mismo orden. De no hacerlo así podrían ocurrir muchos deadlocks (bloqueos mortales) en la Base de Datos.

Conclusión:

En general deberíamos usar transacciones con tratamiento optimista porque eso reduce el tiempo en el cual los registros quedan bloqueados. Si un registro está bloqueado le causa trastornos a los demás usuarios que también quieren modificarlo. El tratamiento pesimista debe usarse cuando no hay alternativa, por ejemplo cuando se requieren números secuenciales que no tengan huecos (números faltantes) entre ellos. Y en ambos casos lo correcto es que las transacciones sean lo más cortas posibles.

Artículos relacionados:

Modos de bloqueo de las transacciones

Bloqueos mortales

Algo más sobre transacciones optimistas y transacciones pesimistas

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

El índice del blog Firebird21