¿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

Como una transacción que terminó anormalmente es desechada

2 comentarios

Si una transacción terminó normalmente entonces es seguro que fue finalizada con un COMMIT o con un ROLLBACK.

Pero ¿y si terminó anormalmente?

Supongamos que ocurrió un corte de la energía eléctrica que apagó al Servidor mientras había una transacción Activa.

¿Cómo se terminará esa transacción?

Como la transacción no finalizó con un COMMIT ni con un ROLLBACK entonces continuará marcada como Activa aunque en realidad ya está muerta porque será imposible finalizarla con un COMMIT y ya nada se puede hacer con ella.

Pero en la Base de Datos sigue estando marcada como Activa.

Hay 2 formas de desechar (marcar como RolledBack)  a una transacción que finalizó anormalmente, y para entender esas dos formas debemos conocer que ocurre cuando una transacción inicia.

  1. Cada vez que una transacción inicia bloquea a su propio Identificador de transacción. O sea, obtiene acceso exclusivo a él. Por lo tanto, cuando se inicia la transacción T1 bloquea al identificador de la transacción T1. Si luego una transacción T2 quiere actualizar o borrar una fila primero verifica si la última versión de esa fila fue creada por una transacción que aún está Activa.  Si ése es el caso entonces trata de bloquear al Identificador de la transacción T1. Si lo consigue es porque la transacción T1 en realidad ya no está Activa, sino que está muerta. Entonces, la transacción T2 cambia el estado de la transacción T1 y lo marca como RolledBack.
  2. Cada vez que una transacción inicia, trata de obtener un acceso exclusivo a la Base de Datos. Si lo consigue es porque ninguna otra transacción está Activa. Y en ese caso cambia el estado de todas las transacciones que estaban marcadas como Activa y les coloca RolledBack. Tanto si consiguió el acceso exclusivo como si no lo consiguió, a continuación obtiene un acceso compartido a la Base de Datos. Por definición, si una transacción tiene acceso compartido entonces ninguna otra transacción podrá obtener un acceso exclusivo, aunque sí podrá tener un acceso compartido.

El método 1. tiene la ventaja de ser muy rápido, pero tiene la desventaja de que la transacción T1 (que finalizó anormalmente) puede estar marcada como Activa durante muchísimo tiempo si ninguna transacción T2 quiere actualizar o borrar las mismas filas que actualizó o borró la transacción T1.

El método 2. tiene la ventaja de que marca como RolledBack a todas las transacciones que finalizaron anormalmente, y lo hace de una sola vez. Su desventaja es que en bases de datos muy grandes y que tienen muchas transacciones terminadas anormalmente se demora bastante más tiempo.

Entonces ¿qué ocurre cuando se inicia el Servidor del Firebird?

Que la primera transacción intentará obtener un acceso exclusivo a la Base de Datos. Si lo consigue (siempre lo debería conseguir), marcará a todas las transacciones que estaban Activa como RolledBack. Y por lo tanto en la Base de Datos ninguna transacción continuará estando marcada incorrectamente como Activa. Eso está muy bien, así debe ser.

El corolario es que para asegurarnos de no tener transacciones muertas (o sea, transacciones marcadas como Activa pero que en realidad no pueden ser finalizadas con un COMMIT) debemos detener y luego reiniciar al Servidor del Firebird.

Conclusión:

Cuando una transacción se inicia su estado siempre es Activa. Si luego finaliza anormalmente (por un corte de la energía eléctrica, por ejemplo) continúa marcada como Activa aunque en realidad está muerta ya que no puede finalizar con un COMMIT.

Está mal que esté marcada como Activa, eso no es lo correcto, y debe corregirse.

Para corregir el estado de la transacción existen dos métodos que emplea el Firebird:

Método 1. Cuando una transacción T2 quiere actualizar o borrar una fila cuya última versión fue creada por una transacción T1 que figura como Activa aunque sin estar Activa, actualiza el estado de la transacción T1 y le pone RolledBack, tal y como debe ser.

Método 2. Cuando una transacción se inicia, trata de obtener acceso exclusivo a la Base de Datos. Si lo consigue es porque no existe otra transacción Activa y entonces cambia el estado de todas las transacciones que estaban marcadas como Activa (incorrectamente, desde luego) a RolledBack, tal y como debe ser.

La desventaja del Método 1. es que una transacción T1 puede continuar marcada como Activa durante mucho tiempo, si ninguna otra transacción T2 quiere actualizar o borrar esas mismas filas. La desventaja del Método 2. es que solamente funciona cuando no hay otras transacciones activas, por lo tanto es inaplicable en Servidores que están encendidos 24/7/365.

La mejor manera de asegurarnos de no tener transacciones cuyo estado está marcado como Activa aunque en realidad están muertas, es detener y luego reiniciar el Servidor del Firebird.

Artículos relacionados:

Entendiendo a las transacciones

Entendiendo los identificadores de las transacciones

Entendiendo las páginas de la Base de Datos

El índice del blog Firebird21

El foro del blog Firebird21

 

 

 

NO RECORD_VERSION es el defecto en el aislamiento READ COMMITTED

5 comentarios

Esto es importantísimo para recordar y tener en cuenta.

En ambas versiones del libro “The Firebird Book” de Helen Borrie, que es la “biblia” del Firebird dice que si una transacción tiene el aislamiento READ COMMITTED su versionado por defecto es RECORD_VERSION.

Eso no es así (se equivocó la “biblia”) porque en realidad el versionado por defecto es NO RECORD_VERSION, tal y como se establece en el documento “Interbase 6.0 Embedded SQL Guide”, página 67 (está en inglés) y en el documento “Firebird 2.5 Language Reference”, página 348 (está en ruso).

Como en “The Firebird Book” decía que el versionado por defecto era RECORD_VERSION el autor de este blog así lo creyó y lo escribió en un artículo previo (que ya está corregido también).

Entonces y para asegurarnos … verifiquemos cual es el versionado por defecto.

Para ello abriremos dos instancias de ISQL y veremos lo que sucede cuando se actualiza una fila de una tabla.

RECORD_VERSION

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

Como se puede ver en la Captura 1., cuando no se especifica el versionado el Firebird asume por defecto que se trata de NO RECORD_VERSION. Eso lo podemos comprobar haciendo el UPDATE de una fila en la instancia ISQL 1 y luego queriendo consultar esa misma fila en la instancia ISQL 2. El mensaje “Lock conflict on no wait transaction….” nos está diciendo que ocurrió un conflicto con otra transacción. ¿Por qué? Porque hay otra transacción que hizo un UPDATE o un DELETE a la fila cuyos datos quisimos consultar y el aislamiento de nuestra transacción es READ COMMITTED y su versionado evidentemente es NO RECORD_VERSION, por eso obtuvimos el mensaje de error.

Establezcamos ahora que la transacción sea READ COMMITTED y su versionado RECORD_VERSION y veamos lo que ocurre.

¿Qué ocurrió? Pues que nuestro SELECT que involucra a la fila que la instancia ISQL 1 está actualizando, esta vez sí tuvo éxito. Como podemos ver en la última fila de la Captura 1., obtuvimos lo que se encuentra en la última versión confirmada (es decir, que finalizó con un COMMIT) de esa fila.

Conclusión:

Es muy importante recordar que por defecto el Firebird abre a las transacciones READ COMMITTED con el versionado de NO RECORD_VERSION y que ese es el versionado que provoca más conflictos.

Como podemos ver en la Captura 1. el versionado NO RECORD_VERSION provocó un conflicto, en cambio el versionado RECORD_VERSION pasó sin problema.

De todo esto podemos sacar dos enseñanzas:

  1. No confiar ciegamente en todo lo que dicen los libros (o los blogs, je), sus autores pueden equivocarse.
  2. Si abrimos una transacción READ COMMITTED y no especificamos el versionado, éste será NO RECORD_VERSION y ese versionado provoca muchos conflictos.

Artículos relacionados:

Entendiendo a las transacciones

El índice del blog Firebird21

El foro del blog Firebird

Error con IN AUTONOMOUS TRANSACTION

2 comentarios

Como sabes, IN AUTONOMOUS TRANSACTION puede llegar a ser muy útil algunas veces porque todo lo que escribamos dentro de esa construcción finalizará con un COMMIT, sin importar como finalice la transacción principal (con un COMMIT o con un ROLLBACK)

Sin embargo … lastimosamente no es perfecta, tiene un problema que puede llegar a ser muy grave:

Dentro de un trigger de la Base de Datos relacionado con una transacción entra en un ciclo infinito.

Lo entenderemos mejor viendo el siguiente código:

CREATE TRIGGER TRANSACCION_INICIA
   ACTIVE ON TRANSACTION START
   POSITION 0
AS
BEGIN
   
   IN AUTONOMOUS TRANSACTION DO BEGIN

   END

END;

No importa lo que escribamos dentro del BEGIN … END de la transacción autónoma, y aún si nada escribimos como en este ejemplo, la Base de Datos se “colgará”. También se colgará si escribimos una sentencia simple (es decir, sin el BEGIN … END)

Los triggers de la Base de Datos en los cuales NUNCA JAMÁS debemos escribir IN AUTONOMOUS TRANSACTION son:

  • TRANSACTION START
  • TRANSACTION COMMIT
  • TRANSACTION ROLLBACK

En cualquiera de ellos, IN AUTONOMOUS TRANSACTION entrará en un ciclo infinito (no me preguntes el motivo, es así y ya. Evidentemente se trata de un “bug”).

Sin embargo, sí es posible usar IN AUTONOMOUS TRANSACTION con los triggers de conexión a la Base de Datos, con ellos no hay problema, es decir que puedes con toda tranquilidad escribirla en:

  • CONNECT
  • DISCONNECT

El siguiente código, sí funcionará, sin problemas:

CREATE TRIGGER CONEXION_INICIA
   ACTIVE ON CONNECT
   POSITION 0
AS
BEGIN
    
   IN AUTONOMOUS TRANSACTION DO BEGIN
    
   END
 
END;

Aquí funciona porque es un trigger de la Base de Datos relacionado con la conexión, con los que no funciona es con los relacionados a la transacción.

Artículos relacionados:

Registrando errores en una tabla de LOG

Otro ejemplo del uso de la tabla de LOG

El índice del blog Firebird21

El foro del blog Firebird21

Usando Firebird durante 24/7/365

5 comentarios

Algunas aplicaciones requieren acceso a las bases de datos durante 24/7/365, o sea durante las 24 horas del día, los 7 días de la semana y los 365 días del año. En otras palabras: sin interrupción, por ningún motivo.

Algunos ejemplos típicos son:

  • Sanatorios, hospitales, centros de salud
  • Farmacias que trabajan las 24 horas
  • Estaciones de servicio (expendio de combustibles)
  • Policía
  • Fuerzas Armadas
  • etc.

Una aplicación desarrollada para ser utilizada por ellos tiene la obligación de poder funcionar sin interrupción en cualquier momento del día. No le puedes decir al Director de un hospital que entre las 22:00 y las 23:00 no pueden registrarse los datos de los pacientes porque hay que hacerle un mantenimiento a la Base de Datos. Ni decirle a un General que si mañana el país es atacado por el enemigo entre las 02:00 y las 03:00 entonces no podrá usar la Base de Datos porque ya tiene mucha basura y que hiciste un proceso para que elimine esa basura pero requieres que nadie esté conectado a la Base de Datos.

¿Y cuál es el problema?

El problema radica en que a las bases de datos de Firebird debe hacérsele una tarea de mantenimiento llamada sweep de vez en cuando. El sweep (barrido, en castellano) tiene como finalidad eliminar a todas las versiones inútiles de las filas que fueron modificadas o borradas. Cada vez que una fila es modificada o borrada se crea una versión de esa fila llamada delta. La parte positiva de esto es que puede realizarse un ROLLBACK y dejar a la fila como se encontraba anteriormente si el usuario cambió de idea. La parte negativa es que se va acumulando basura y todas las operaciones se van volviendo más lentas y por lo tanto esa basura debe ser eliminada alguna vez.

 Y el sweep puede demorar mucho tiempo.

¿Cuándo se inicia el sweep?

El sweep puede iniciarse automáticamente (que es su valor por defecto) cuando la diferencia entre la OST (Oldest Snapshot Transaction) y la OAT (Oldest Active Transaction) es mayor que el intervalo predefinido del sweep. Ese intervalo es de 20.000 por defecto pero puede modificarse.

Si el intervalo del sweep es de 0, entonces el sweep nunca se iniciará automáticamente. Eso implica que hay que iniciarlo manualmente. Para ello se usa el programa GFIX con la opción –sweep. En este caso lo recomendable es que nadie esté usando la Base de Datos mientras se realiza el sweep.

 ¿Qué ocurre cuándo se inicia el sweep?

Que la transacción que hizo iniciar al sweep no empezará hasta que el sweep finalice. Supongamos que el intervalo del sweep es de 20.000, entonces el desafortunado usuario que quiso iniciar una transacción cuando OST – OAT fue de 20.001 tendrá que esperar hasta que finalice el sweep antes de que su transacción pueda empezar. A quienes iniciaron sus transacciones antes que él no les afectará, pero a él sí. Y si justo es un General de muy malas pulgas, entonces…

 ¿Se puede evitar que se inicie el sweep sin perjudicar el rendimiento de la Base de Datos?

Sí.

La buena noticia es que si las aplicaciones que usan la Base de Datos están correctamente diseñadas entonces nunca será necesario realizar el sweep.

La basura que se encuentra dentro de una Base de Datos puede deberse a los siguientes motivos:

  • Una operación de UPDATE que finalizó con un COMMIT
  • Una operación de DELETE que finalizó con un COMMIT
  • Una operación de UPDATE que finalizó con un ROLLBACK
  • Una operación de DELETE que finalizó con un ROLLBACK

La basura dejada por las transacciones que finalizaron con un COMMIT es recolectada (garbage collection) automáticamente cuando se hace un SELECT a esas filas. Por lo tanto, para eliminar toda la basura de una tabla que fue dejada por los COMMIT se puede escribir un simple comando: SELECT * FROM MiTabla y listo: esa tabla ya no tendrá basura dejada por los COMMIT.

La basura dejada por el ROLLBACK sin embargo, solamente puede ser eliminada con un sweep.

Por lo tanto la solución es muy simple y muy sencilla: No usar ROLLBACK.

¿Y cómo se puede evitar usar ROLLBACK?

Hay varias alternativas posibles, una posible solución sería esta:

  • Haces un SELECT a la fila que te interesa, por supuesto finalizando ese SELECT con un COMMIT. El resultado de ese SELECT lo guardas en una tabla temporal que no es de Firebird, por ejemplo en una tabla .DBF
  • Le muestras la fila de la tabla .DBF al usuario
  • Le permites que cambie los campos o que borre la fila
  • Si decide Guardar los cambios, entonces haces un UPDATE a la tabla de Firebird con los valores que se ven en la pantalla y luego el COMMIT correspondiente
  • Si decide Cancelar los cambios, entonces allí termina todo, no tocas la tabla de Firebird para nada

Como ves, en estos casos se trabaja un poco más pero te aseguras que todas las transacciones terminen con un COMMIT.

Recolectando la basura

Adicionalmente, en tu aplicación deberías tener un proceso que se encargue de hacer un SELECT * a todas las tablas de tu Base de Datos. Cuando el usuario ejecuta ese proceso entonces se hace un SELECT * FROM MiTabla1, SELECT * FROM MiTabla2, SELECT * FROM MiTabla3, etc.

Recordatorio:

La técnica mostrada en este artículo es necesaria solamente para las bases de datos que deben estar activas durante 24/7/365. En los demás casos se puede seguir el procedimiento normal de hacer el sweep automáticamente o manualmente.

Conclusión:

Las bases de datos de Firebird pueden perfectamente ser usadas en entornos que las requieren abiertas durante las 24 horas, los 7 días de la semana y los 365 días del año. Lo que debemos tener en cuenta en esos casos es que deberíamos evitar el sweep porque el sweep puede demorar mucho tiempo en bases de datos muy grandes. La forma de evitar el sweep es nunca terminar las transacciones con ROLLBACK porque las transacciones que terminan con ROLLBACK dejan basura que solamente puede ser eliminada con un sweep, en cambio la basura dejada por las transacciones que terminan con un COMMIT puede ser eliminada con un comando SELECT * FROM MiTabla.

 Entonces, el problema es causado por la forma en que Firebird actúa cuando se hace un UPDATE o un DELETE que finalizaron con un ROLLBACK pero puede ser solucionado si la aplicación finaliza todas las transacciones con un COMMIT.

Artículos relacionados:

La arquitectura MGA

Entendiendo a las transacciones

Entendiendo a los identificadores de las transacciones

Entendiendo sweep y garbage collection

El índice del blog Firebird21

El foro del blog Firebird21

 

Verificando periódicamente los identificadores de las transacciones

14 comentarios

Como ya seguramente sabes, puedes verificar cuales son los valores actuales de los identificadores de las transacciones con el comando GSTAT – h, algo como:

VERIFICAR1

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

Si nadie está usando la Base de Datos y si todas tus transacciones finalizan con un COMMIT o con un ROLLBACK entonces siempre deberías ver algo así: la diferencia entre la “Oldest transaction” y la “Oldest active” nunca debería ser mayor que 1 (uno) y la diferencia entre la “Oldest snapshot” y la “Next transaction”  tampoco debería ser mayor que 1 (uno). Por lo tanto en ningún caso la diferencia entre los distintos identificadores debería ser mayor que 2 (dos).

Tú has verificado exhaustivamente tus aplicaciones, estás 100% seguro de que todas las transacciones, sin excepción, finalizan con un COMMIT o con un ROLLBACK, entonces no deberías molestarte en verificar los identificadores de las transacciones porque siempre tendrán una diferencia de 1 ó 2 entre ellos, ¿verdad?

No, falso.

Sería verdad si todas las aplicaciones siempre se cierran correctamente, pero no puedes asegurar de que eso siempre ocurrirá. Si hubo un corte de la energía eléctrica que  afectó a una computadora que tenía una transacción abierta o si un usuario (muy inteligente él, claro) hizo un CTRL-ALT-DEL o apretó el botón de “Reset” de la computadora entonces la aplicación que el inteligente usuario estaba usando sin dudas que finalizó pero las transacciones que esa aplicación tenía abiertas, no. No finalizaron.

Al salir de la aplicación de manera incorrecta (por medio de un CTRL-ALT-DEL, por ejemplo) la aplicación terminó, pero todas las transacciones que esa aplicación tenía abiertas continúan abiertas.

Y pasa un día, pasan dos días, pasan quinientos días y esas transacciones continuarán abiertas.

El Firebird no tiene un time-out, o sea un tiempo máximo para que una transacción continúe abierta. Una transacción “normalmente” solamente puede finalizar con un COMMIT o con un ROLLBACK.

¿Y cuál es el problema de que una transacción que nadie usa continúe abierta?

Que el Firebird no hará una limpieza automática (llamada “garbage collection” o “recolección de basura” en castellano) de las transacciones más nuevas que la OAT (Oldest Active Transaction).

Eso implica que toda la basura que fueron dejando las transacciones más nuevas (siempre se deja basura cuando se modifica o se borra un registro) quedarán dentro de la Base de Datos.

Si la OAT ocurrió hace un mes entonces toda la basura que las siguientes transacciones fueron dejando al modificar o borrar registros están ahora dentro de la Base de Datos.

Y tener basura dentro de la Base de Datos no te provee de algún beneficio y sí te causa problemas: todo es más lento de lo que debería ser. Y cuanta más basura se vaya acumulando, más y más lentas serán todas las operaciones.

Por lo tanto, aunque tú estés 100% seguro de que todas tus transacciones finalizan correctamente con un COMMIT o con un ROLLBACK, como debe ser, siempre debes verificar los identificadores de las transacciones porque nunca podrás descartar que no haya ocurrido un corte de la energía eléctrica o que un usuario no haya presionado las teclas CTRL-ALT-DEL.

Conclusión:

Aunque estés 100% seguro de que todas las transacciones que abren tus aplicaciones son luego cerradas con un COMMIT o con un ROLLBACK igualmente debes verificar periódicamente los identificadores de las transacciones con el comando GSTAT -h porque si por cualquier motivo una aplicación finalizó incorrectamente (por medio de un CTRL-ALT-DEL, por ejemplo) entonces esa aplicación que finalizó incorrectamente pudo haber dejado transacciones abiertas.

Y las transacciones que están abiertas pero que nadie las usa (porque la aplicación que las creó ya finalizó hace mucho) solamente causan problemas.

Por lo tanto, si hay transacciones abiertas pero no usadas debes eliminarlas.

Dependiendo del tamaño de tu Base de Datos y de la cantidad de usuarios concurrentes que puede tener deberías ejecutar el comando GSTAT -h cada día o cada semana o cada quince días, etc., pero debes hacerlo, no te olvides de hacerlo. O alguna vez podrías encontrarte con la desagradable sorpresa que la diferencia entre los identificadores de las transacciones es inmensa.

Artículos relacionados:

Entendiendo los identificadores de las transacciones

Conociendo el programa que mantiene una transacción abierta

El índice del blog Firebird21

 

Entendiendo sweep y garbage collection

7 comentarios

Como habíamos visto en este artículo:

https://firebird21.wordpress.com/2013/06/14/la-arquitectura-mga/

cuando se actualiza (UPDATE) o se borra (DELETE) un registro el Firebird guarda en la Base de Datos la versión anterior de ese registro. ¿Para qué hace eso? Para poder revertir al registro original cuando se hace un ROLLBACK.

Un ROLLBACK se hace si ocurrió un error o si el usuario cambió de idea y no quiere guardar lo último que hizo, o si se cerró anormalmente la conexión.

Por lo tanto, cada comando UPDATE y cada comando DELETE le agrega un registro a la tabla respectiva (en el caso del DELETE el nuevo registro solamente tiene un marca que le indica al Firebird que ese registro está “borrado”).

Veamos un ejemplo de UPDATE:

El precio de “Televisor Toshiba de 20 pulgadas” es de 120 dólares. Como se está vendiendo poco ya que la mayoría de la gente ahora prefiere televisores de más pulgadas se decidió disminuir su precio a 100 dólares, tratando de conseguir que aumenten las ventas al disminuir el precio.

Entonces, en nuestra tabla de PRODUCTOS, luego del correspondiente UPDATE, tendríamos:

SWEEP1

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

Ahora hay dos posibilidades:

  1. Que la transacción termine con un COMMIT exitoso
  2. Que la transacción termine con un ROLLBACK

Si la transacción terminó con un COMMIT exitoso entonces el registro antiguo, el que tiene el TID 143 ya no sirve, es inútil. Pero el Firebird no lo elimina de la Base de Datos, lo deja ahí. Y eso implica que está ocupando espacio inútilmente.

Si la transacción terminó con un ROLLBACK entonces el registro nuevo, el que tiene el TID 279 ya no sirve, es inútil. Pero el Firebird no lo elimina de la Base de Datos, lo deja ahí. Y eso implica que está ocupando espacio inútilmente.

De lo anterior se deduce que cada vez que escribes el comando UPDATE dejas un registro inservible dentro de la Base de Datos. O el original queda inservible o el actualizado queda inservible.

Hay por lo tanto dos tipos de registros inservibles:

  • Los dejados por los COMMIT
  • Los dejados por los ROLLBACK

¿Hay diferencia si el registro inservible fue dejado por un COMMIT o por un ROLLBACK?

Sí, la diferencia es que los registros inservibles dejados por los COMMIT pueden ser eliminados automáticamente durante la recolección de la basura (ver más abajo), en cambio los dejados inservibles por los ROLLBACK nunca son eliminados automáticamente.

¿Qué es la “basura”?

Esos registros inservibles, inútiles, que fueron dejados por los UPDATE o por los DELETE siguen ocupando espacio dentro de la Base de Datos, pero como ya son inservibles y totalmente inútiles se les llama “basura”.

¿Cuál es el problema con la basura?

Que ocupa espacio inútilmente dentro de la Base de Datos y por lo tanto ésta es más grande de lo que debería ser y además se vuelve cada vez más lenta.

¿Qué significa “garbage collection”?

En castellano: recolección de la basura. Cuando se recolecta la basura todos esos registros inservibles dejados por los COMMIT son eliminados permanentemente y definitivamente de la Base de Datos. Como consecuencia de ello, la Base de Datos queda con espacio reutilizable y además las operaciones dentro de ella (INSERT, UPDATE, DELETE, FETCH, SELECT) se realizan más rápidamente.

¿Cuándo se realiza la “garbage collection”?

Cada vez que un registro es “tocado” por un SELECT toda la basura relacionada con ese registro como consecuencia de los COMMIT es eliminada. En otras palabras, si al escribir un SELECT un registro está en el resultado obtenido entonces toda la basura relacionada con ese registro, producida por los  COMMIT, es eliminada.

Fíjate que solamente el SELECT elimina a la basura, los demás comandos: INSERT, UPDATE, DELETE, no la eliminan, la dejan así mismo como estaba.

¿Cuál es la forma más rápida de eliminar toda la basura de una tabla?

Escribir el comando:

SELECT
   COUNT(*)
FROM
   MiTabla

Lo que hace ese comando es contar la cantidad de registros que tiene la tabla pero para hacerlo debe recorrer la tabla desde el primer registro hasta el último registro porque el Firebird no guarda en alguna parte la cantidad de registros de la tabla. Por lo tanto, todos los registros de esa tabla están involucrados en el SELECT y por consiguiente en todos esos registros es recolectada la basura. Si la tabla tiene 12.000 registros entonces SELECT COUNT(*) recorre los 12.000 registros y el “garbage collection” elimina toda la basura que haya encontrado en cualquiera de esos 12.000 registros.

Recuerda que esta forma de eliminar a la basura solamente elimina a la basura que dejaron los COMMIT, la basura dejada por los ROLLBACK no se puede eliminar así.

¿Cuál es el problema con recolectar la basura usando SELECT?

Que para recolectar la basura de todas las tablas debes hacer SELECT COUNT(*) en todas las tablas o esperar que algún SELECT involucre a los registros que tienen basura. A veces, puede ocurrir que se hizo el UPDATE de un registro pero nunca se hizo un SELECT que involucre a ese registro y por lo tanto la basura que dejó el UPDATE permanece dentro de la Base de Datos.

Además, no te olvides que cuando se ejecuta un SELECT solamente se recolecta la basura que dejaron los COMMIT, la basura dejada por los ROLLBACK continúa allí.

¿Qué es el sweep?

El sweep (en castellano: barrido, de barrer con una escoba) es un proceso que recorre toda la Base de Datos y elimina toda la basura que se encuentra en ella, tanto la proveniente de los COMMIT como la proveniente de los ROLLBACK. Cuando el sweep finaliza la Base de Datos está bien limpia, sin basura.

¿Cuándo se realiza el sweep?

El sweep se puede realizar automáticamente o manualmente. Automáticamente es cuando lo inicia el propio Firebird y manualmente es cuando lo inicia un programa (por ejemplo: GFIX y GBAK pueden iniciar el sweep).

Sweep automático

Cada Base de Datos tiene una entrada denominada “sweep interval” o sea intervalo del sweep. Es un número de tipo INTEGER  y por lo tanto su valor puede estar entre 0 y 2.147.483.647. Por defecto su valor es 20.000.

Cuando la diferencia entre la OST y la OAT es mayor que el intervalo del sweep, se inicia el sweep. Por ejemplo:

OST = 45.201

OAT = 25.200

Sweep interval = 20.000

Diferencia = OST – OAT = 45.201 – 25.200 = 20.001

Como la diferencia es mayor que el intervalo del sweep, se inicia el sweep automático.

Si no se quiere realizar un sweep automático entonces el intervalo del sweep debe ser 0 (cero)

¿Cuál es el problema con el sweep?

Que este proceso de recorrer toda la Base de Datos y de eliminar toda la basura que hay en ella es lento, en algunos casos extremadamente lento y los usuarios se quejan de que todas las operaciones (INSERT, UPDATE, DELETE, FETCH, SELECT) se realizan muy despaciosamente. Y por supuesto podría ocurrir que el sweep automático se inicie cuando los usuarios están más apurados, más impacientes y por culpa del sweep todas sus transacciones son más lentas que una tortuga con tres patas. Es por lo tanto normal que el DBA (Administrador de la Base de Datos) ponga el intervalo del sweep en 0 (cero) y de esa manera realizará el sweep manualmente, en momentos en que nadie está usando la Base de Datos (o cuando muy pocos usuarios la están usando).

Pero …. a veces el DBA se olvida de que dejó el intervalo del sweep en cero y la Base de Datos se va volviendo cada vez más grande y más lenta por toda la basura que tiene acumulada.

En otras palabras, se necesita un DBA con cerebro, algo que no siempre se consigue.

Aprovechar el backup para hacer el sweep

Cuando haces un backup usando el programa GBAK tienes la opción de que también se haga el sweep de la Base de Datos. Esa es la opción por defecto y la recomendable. Así, cuando finaliza el programa GBAK puedes estar seguro de que el archivo de backup generado no tiene basura. Recuerda: la Base de Datos original continúa con toda la basura que tenía, el backup generado es el que no tiene basura.

Usando el programa GFIX para hacer el sweep

Una de las opciones de este programa nos permite pedirle que haga el sweep:

GFIX -sweep -user SYSDBA -password masterkey MiBaseDatos

Un pequeño “defecto” que tiene el programa GFIX es que no te avisa que finalizó exitosamente y eso confunde a los principiantes. O sea, si termina sin ningún mensaje significa que todo estuvo ok, si ocurrió algún problema entonces termina con un mensaje de error.

Conclusión:

El uso normal de la Base de Datos va dejando basura dentro de ella. Esa basura debe ser eliminada en algún momento porque de no eliminarse solamente causará que la Base de Datos tenga un tamaño mayor que el necesario y además que todas las transacciones sean más lentas de lo que deberían. La eliminación de la basura puede hacerse en forma automática (cuando el intervalo del sweep es mayor que cero y la diferencia entre la OST y la OAT es mayor que ese intervalo) o en forma manual (cuando el intervalo del sweep es cero y en ese caso hay que ejecutar el programa GFIX). También se elimina la basura cuando se hace un backup usando el programa GBAK y no se especifica la opción -garbage collection.

Artículos relacionados:

La arquitectura MGA

Entendiendo a las transacciones

Entendiendo a los identificadores de las transacciones

El índice del blog Firebird21

Older Entries