¿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

Anuncios

Evitando conflictos en las transacciones

2 comentarios

A las bases de datos de Firebird normalmente se conectan muchos usuarios y en ocasiones quieren actualizar o borrar la misma fila y al mismo tiempo y eso puede causar conflictos. ¿Cómo los evitamos?

Primero, debemos tener presente que es normal que existan conflictos y que debemos prepararnos para lidiar con ellos.

Segundo, en muchos casos es posible evitar esos conflictos si diseñamos inteligentemente la Base de Datos.

Evitando los conflictos

Los conflictos solamente pueden ocurrir cuando dos o más usuarios quieren actualizar o borrar la misma fila y al mismo tiempo. O sea, cuando se usa el comando UPDATE o el comando DELETE.

Por lo tanto, lo mejor es evitar usar esos comandos y solamente usar el comando INSERT.

¿Y cómo evitamos usar el comando UPDATE o el comando DELETE?

Hay dos técnicas:

  1. Guardar los movimientos. Sería el caso si por ejemplo tenemos una tabla de PRODUCTOS y se hacen compras y ventas de esos productos. Mientras se están haciendo las compras y las ventas jamás actualizamos la tabla de PRODUCTOS, solamente insertamos filas a nuestra tabla de movimientos.
  2. Usando una tabla auxiliar. Sería el caso cuando no guardamos los datos en una tabla de movimientos. Por ejemplo, cuando queremos cambiar el precio de venta de un producto.

En ambos casos, esperamos que nadie esté conectado a la Base de Datos (o al menos, que nadie esté usando la tabla de PRODUCTOS) y mediante un stored procedure hacemos las actualizaciones correspondientes.

En otras palabras, sí usaremos el comando UPDATE o el comando DELETE pero los usaremos cuando estamos 100% seguros de que no pueden ocurrir conflictos.

Ventaja de evitar los conflictos:

  • El trabajo de los usuarios que registran movimientos o actualizaciones es más rápido, porque nunca se encontrarán con una situación de tener que esperar o de deadlock (o sea, una actualización o borrado que falló).

Desventaja de evitar los conflictos:

  • El usuario que debe consultar los datos (mediante el comando SELECT) obtendrá más lentamente los resultados porque los datos se encontrarán en dos o más tablas, no solamente en una tabla.

Conclusión:

Lo normal es que sea preferible evitar los conflictos, ya que los usuarios que registran movimientos o actualizaciones constantemente están trabajando y los conflictos los afectarán negativamente. Además generalmente son muchos más que los usuarios que solamente consultan datos. Quienes necesitan consultar datos generalmente son muy pocas personas y además lo hacen esporádicamente, y con la gran velocidad de las computadoras actuales solamente podrían tardar unos segundos más en obtener un informe, así que no vale la pena preocuparse demasiado por ellos.

Por lo tanto, en el trabajo del día a día es conveniente usar solamente el comando INSERT, y usar el comando UPDATE o el comando DELETE en procesos de actualización que se realizan cuando nadie está usando la Base de Datos (o cuando nadie está usando la tabla que será actualizada).

Si un usuario no puede grabar una fila porque otro usuario tiene bloqueada la fila, eso es muy molesto y debemos hacer lo posible por evitar ese tipo de situaciones.

Artículos relacionados:

Bloqueos mortales

Modos de bloqueo de las transacciones

Transacciones optimistas y transacciones pesimistas

Usando transacciones con acceso exclusivo a tablas

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

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