Se llama bloqueo mortal (deadlock, en inglés) cuando una transacción quiere actualizar o borrar una fila y es rechazada.

Si una transacción (T1) está actualizando o borrando una fila y antes de que finalice con un COMMIT o con un ROLLBACK otra transacción (T2) quiere actualizar o borrar esa misma fila eso causará una colisión, un choque, entre ambas transacciones.

Esto es algo normal cuando trabajamos con bases de datos y nuestras aplicaciones deben estar preparadas para lidiar con ese problema.

Puedes fácilmente ver y entender lo que sucede siguiendo estos pasos:

  1. Abre una instancia de ISQL y conéctacte a una Base de Datos
  2. Actualiza una tabla, por ejemplo escribiendo: UPDATE PRODUCTOS SET PRD_NOMBRE = ‘PRIMERA INSTANCIA’  WHERE PRD_IDENTI = 8;
  3. No escribas ni COMMIT ni ROLLBACK  porque si lo haces estarás terminando la transacción y lo que queremos es que se quede abierta
  4. Abre otra instancia de ISQL y conéctate a la misma Base de Datos
  5. Actualiza la misma fila de la misma tabla, escribiendo algo como: UPDATE PRODUCTOS SET PRD_NOMBRE = ‘SEGUNDA INSTANCIA’ WHERE PRD_IDENTI = 8;
  6. Si el modo de bloqueo de la segunda transacción (T2) es WAIT entonces T2 se quedará esperando y esperando y esperando.
    • La transacción T2 solamente terminará después que la transacción T1 termine con un COMMIT o con un ROLLBACK o cuando hayan transcurrido los segundos especificados en la cláusula LOCK TIMEOUT. Si la transacción T2 es READ COMMITTED entonces podrá hacer un COMMIT después que la transacción T1 termine, pero nunca antes.
  7. Si el modo de bloqueo de la transacción T2 es NO WAIT entonces inmediatamente recibirá un mensaje de error, para que sepa que no se pudo realizar la actualización porque la transacción T1 tiene bloqueada a esa fila. En este caso ocurrirá un bloqueo mortal.
    • En este caso cualquier intento de hacer un COMMIT fallará. Si se desea grabar la fila de todas maneras, la única solución es abrir una nueva transacción (T3) y reintentar el UPDATE o el DELETE.

Si diseñamos bien nuestra Base de Datos muy raramente dos o más usuarios deberían estar actualizando o borrando la misma fila, sin embargo a veces es inevitable que lo hagan ¿cómo reaccionamos en ese caso?

Primero, tener bien presente que usar la cláusula WAIT a secas es muy peligroso porque si quien inició la transacción T1 se fue de vacaciones sin cerrar esa transacción, nadie más podrá actualizar esa/s fila/s hasta que regrese. La solución a este problema es siempre usar la cláusula LOCK TIMEOUT cuando se usa WAIT, una espera de 10 segundos suele ser más que suficiente para la gran mayoría de los casos.

Segundo, en los programas de ABM (Agregar/Borrar/Modificar) generalmente lo mejor es que el aislamiento sea READ COMMITTED, entonces cuando termine la transacción T1 la transacción T2 actualizará la fila.

Tercero, para disminuir la probabilidad de conflictos las transacciones deben ser lo más cortas posibles. Si tu transacción tarda 1 segundo en completarse entonces difícilmente encontrarás conflictos pero si tarda 30 minutos, la probabilidad será grandísima. Transacciones cortas te harán la vida más fácil.

Cuarto, en la gran mayoría de los casos, si lo único que hará tu transacción será consultar datos entonces establecer que su acceso sea READ ONLY y que su aislamiento sea SNAPSHOT y que su modo de bloqueo sea WAIT es lo recomendado porque así será más rápida.

Quinto, no te olvides de cerrar todas tus transacciones, sea con un COMMIT o con un ROLLBACK y eso incluye a las transacciones que solamente hacen un SELECT. En Firebird inclusive los SELECTs están siempre dentro de una transacción y es un error muy común de los principiantes dejar abiertas las transacciones que solamente tienen SELECTs porque creen que al finalizar el SELECT también finaliza la transacción y eso es falso.

Sexto, como ya hemos visto los COMMITs pueden fallar porque se intentó actualizar o borrar una fila que estaba bloqueada por otra transacción. Por lo tanto siempre hay que verificar que el COMMIT se haya completado con éxito.