Respuestas a más preguntas sobre transacciones (4)

1 comentario

¿Estás respondiendo las preguntas sobre transacciones?

Si lo estás haciendo, felicitaciones, si no lo estás haciendo, …. deberías, para aprender más.

Veamos cuales son las respuestas correctas del artículo: Más preguntas sobre transacciones (4)

Pregunta 1. Si una transacción solamente hará SELECTs ¿cuál debe ser su acceso?

[READ ONLY]     [READ WRITE]     [Es indiferente]

La respuesta correcta: Es indiferente. ¿Por qué? porque en ambos casos podrá hacer los SELECTs, sin embargo es preferible que sea READ ONLY porque las transacciones READ ONLY terminan más rápido que las transacciones READ WRITE debido a que el Firebird realiza menos tareas, pero no es obligatorio que sea READ ONLY.

Pregunta 2. Si una transacción solamente hará INSERTs ¿cuál debe ser su acceso?

[READ ONLY]     [READ WRITE]     [Es indiferente]

En este caso la transacción debe ser READ WRITE porque no se puede realizar un INSERT (ni un UPDATE, ni un DELETE) en las transacciones READ ONLY.

Pregunta 3. Si una transacción hará SELECTs e INSERTs, ¿cuál debe ser su acceso?

[READ ONLY]     [READ WRITE]     [Es indiferente]

Debe ser READ WRITE sí o sí, porque no se puede realizar un INSERT (ni un UPDATE, ni un DELETE) en una transacción READ ONLY.

Pregunta 4. Si una transacción quiere conocer lo que otras transacciones que finalizaron con COMMIT hicieron. ¿Cuál debe ser su aislamiento?

[READ COMMITTED]     [SNAPSHOT]     [Es imposible, nunca puede conocer lo que otras transacciones hicieron]

El aislamiento READ COMMITTED permite conocer lo que otras transacciones que ya finalizaron con un COMMIT hicieron. El aislamiento SNAPSHOT no permite tal cosa.

Pregunta 5. Si una transacción T1 ha bloqueado a una fila y una transacción T2 quiere actualizar a esa misma fila. ¿Cuál debe ser el modo de bloqueo de la transacción T2 para que pueda hacerlo?

[WAIT]     [NO WAIT]     [Cualquiera, es indiferente]     [Nunca la transacción T2 podrá actualizar esa fila]

Debe ser WAIT y entonces la transacción T2 esperará hasta que la transacción T1 finalice. Si el modo de bloqueo de la transacción T2 es NO WAIT entonces inmediatamente obtendrá una excepción y nunca podrá actualizar a esa fila.

Pregunta 6. Si una transacción T1 ha bloqueado a una fila, y quieres que la transacción T2 pueda ver la última versión confirmada de esa fila. ¿Cuál versión de registro debe tener la transacción T2?

[RECORD_VERSION]     [NO RECORD_VERSION]     [Es indiferente]

Debes usar RECORD_VERSION, porque en este caso la transacción T2 podrá ver la última versión confirmada de la fila. Si usas NO RECORD_VERSION entonces la fila que actualizó la transacción T1 será inaccesible para la transacción T2, inclusive para lectura.

Pregunta 7. Si una transacción T1 ha bloqueado una fila y no quieres que la transacción T2 pueda ni siquiera ver la última versión confirmada de esa fila ¿Cuál versión de registro debe tener la transacción T2?

[RECORD_VERSION]     [NO RECORD_VERSION]     [Es indiferente]

Debe ser NO RECORD_VERSION porque en este caso si una fila está bloqueada por una transacción las demás transacciones no pueden acceder a esa fila, ni siquiera para lectura.

Pregunta 8. Si una transacción quiere tener acceso exclusivo a todas las tablas que utiliza ¿puede hacerlo?

[Sí]     [No]

La respuesta es . En ese caso su aislamiento debe ser SNAPSHOT TABLE STABILITY. Si la transacción pudo iniciarse (no siempre podrá, otras transacciones podrían impedírselo) entonces tendrá acceso exclusivo.

Pregunta 9. Si una transacción quiere tener acceso exclusivo solamente a algunas de las tablas que utiliza ¿puede hacerlo?

[Sí]     [No]

La respuesta es . En ese caso debe escribir RESERVING y la lista de las tablas a las que quiere acceder de forma exclusiva.

Artículos relacionados:

Entendiendo a las transacciones

3 preguntas sobre transacciones

Respuestas a las 3 preguntas sobre transacciones

Más preguntas sobre transacciones (2)

Respuestas a más preguntas sobre transacciones (2)

Más preguntas sobre transacciones (3)

Respuestas a más preguntas sobre transacciones (3)

Más preguntas sobre transacciones (4)

El índice del blog Firebird21

El foro del blog Firebird21

Anuncios

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

Evitando actualizaciones concurrentes del mismo registro

2 comentarios

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

Evitando conflictos en las transacciones

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

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

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

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

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

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

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

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

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

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

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

El algoritmo entonces sería el siguiente:

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

¿Qué se hizo aquí?

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

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

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

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

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

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

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

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

Observaciones:

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

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

Conclusión:

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

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

Artículos relacionados:

Entendiendo a las transacciones

Modos de bloqueo de las transacciones

Transacciones optimistas y transacciones pesimistas

Algo más sobre transacciones optimistas y transacciones pesimistas

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

Evitando conflictos en las transacciones

Entendiendo las páginas de la Base de Datos

El índice del blog Firebird21

El foro del blog Firebird21