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

 

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