MySQL vs MariaDB: Interbloqueos — ¿Cuál es más fácil de depurar cuando el sitio arde?

¿Te fue útil?

Los interbloqueos son el tipo de problema de base de datos que aparece justo cuando ya estás atendiendo un pager, un “solo estoy comprobando” del CEO
y un panel lleno de rojo. Las consultas se quedan colgadas, los workers se acumulan y de repente tu sistema “altamente disponible” es altamente disponible para
recibir quejas.

La pregunta práctica no es “qué es un interbloqueo”. Ya lo sabes. La cuestión es: cuando la producción está en llamas, ¿qué motor te da
pistas mejores y más rápidas
— MySQL o MariaDB — y qué deberías hacer primero, segundo y tercero para detener la hemorragia.

El veredicto directo: quién es más fácil de depurar

Si ejecutas MySQL moderno (8.0+), MySQL suele ser más fácil de depurar respecto a interbloqueos y esperas de bloqueo en el momento,
porque Performance Schema es una fuente de verdad más consistente y rica: quién tiene qué bloqueo, quién espera, el texto SQL,
qué índices, qué transacciones. No es perfecto, pero es operativamente cohesivo. Puedes crear memoria muscular y runbooks fiables.

MariaDB también puede ser perfectamente depurable—especialmente si tienes disciplina para habilitar la instrumentación y los logs adecuados—pero
en la práctica es más fácil acabar con observabilidad parcial: suficiente para saber que estás atascado, no suficiente para saber por qué.
Además: el ecosistema de MariaDB tiene más variación (forks/engines/capas de replicación como Galera son más comunes),
lo que significa que tu historia “estándar” de interbloqueos es menos estándar.

Aquí está la regla práctica que uso:

  • Si ya tienes dashboards y runbooks basados en Performance Schema, MySQL 8.0 tiende a ganar en rapidez para llegar a la causa raíz.
  • Si estás en MariaDB con Galera, no solo depuras interbloqueos de InnoDB: a menudo depuras conflictos de certificación, que parecen interbloqueos para la app y son peores.
  • Si confías solo en “SHOW ENGINE INNODB STATUS”, ambos te traicionarán tarde o temprano. Es un artefacto para el incidente, no una estrategia de monitorización.

Orientación con criterio: para equipos que no tienen observabilidad de bases de datos madura, elige la plataforma que haga más fácil obtener
respuestas con la menor cantidad de hacks específicos. Eso suele ser MySQL 8.0 con Performance Schema y un logging sensato.

Una cita que debe estar en la cabeza de todo ops:
“La esperanza no es una estrategia.” —General Gordon R. Sullivan

Broma #1: Un interbloqueo es solo dos transacciones teniendo una reunión y acordando nunca ceder.

Datos interesantes y contexto histórico (lo que importa a las 03:00)

  • MariaDB se creó en 2009 tras la adquisición de Sun por Oracle (y por tanto MySQL). Esa decisión del fork aún resuena en herramientas y valores por defecto.
  • InnoDB se convirtió en el motor por defecto en MySQL 5.5. Los sistemas más antiguos que “crecieron” antes de 5.5 a menudo mantienen hábitos de la era MyISAM que causan dolor de bloqueo en entornos modernos.
  • MySQL 8.0 hizo de Performance Schema el centro de gravedad para la instrumentación. Por eso es el primer lugar donde aterrizan los runbooks modernos de MySQL.
  • MariaDB reemplazó históricamente InnoDB por XtraDB (y más tarde se acercó nuevamente). Dependiendo de la versión, verás diferentes nombres de variables y comportamientos alrededor de la instrumentación.
  • Los interbloqueos son una señal de corrección, no de fallo: InnoDB detecta ciclos y revierte una víctima para mantener el sistema en movimiento. El fallo es cuando tu app no puede reintentar de forma segura.
  • Los gap locks y next-key locks son una fuente frecuente de “interbloqueo sorpresa” bajo REPEATABLE READ. Muchos equipos solo los conocen después de un incidente.
  • La replicación estilo Galera (común en despliegues de MariaDB) puede devolver errores de conflicto que imitan interbloqueos en la capa de aplicación, incluso sin un ciclo clásico de InnoDB.
  • El esquema sys de MySQL (una capa de vistas sobre Performance Schema) facilitó las consultas “qué está pasando ahora” para humanos; cambió cómo los operadores actúan bajo presión.

Cómo se ven los interbloqueos en sistemas de producción reales

Los dos modos de fallo que los operadores confunden (y no deberían)

En el chat del incidente, escucharás “interbloqueo” usado para dos cosas distintas:

  1. Interbloqueo (ciclo): InnoDB detecta un ciclo y aborta una transacción rápidamente. Ves el error 1213: Deadlock found when trying to get lock.
  2. Timeout de espera de bloqueo (cola): No hay ciclo, solo una espera larga. Eventualmente el que espera se rinde con el error 1205: Lock wait timeout exceeded.

Operativamente, requieren instintos diferentes. Un verdadero interbloqueo suele ser un patrón (dos rutas de código tomando bloqueos en órdenes distintas).
Un timeout de espera suele ser un problema de fila caliente / índice caliente / transacción larga, a veces combinado con índices faltantes.
Ambos pueden ocurrir al mismo tiempo durante un outage, que es cómo terminas gritando al gráfico equivocado.

Por qué “pero son solo dos UPDATEs” es una trampa

InnoDB no bloquea “filas” de la manera que los desarrolladores imaginan. Bloquea registros de índice, puede bloquear huecos entre ellos,
y lo hace dependiendo del nivel de aislamiento y la ruta de acceso. Si estás escaneando un rango de índice, puedes bloquear mucho más de lo que pensabas.
Si estás actualizando un índice secundario, podrías tomar bloqueos que no habías previsto.

Qué hace que los interbloqueos sean dolorosos bajo carga

  • Tormentas de reintentos: tu app reintenta transacciones interbloqueadas agresivamente, empeorando la contención de bloqueos.
  • Transacciones largas: una sola transacción lenta mantiene bloqueos más tiempo, aumentando la probabilidad de ciclos.
  • Colapso por cola: los hilos se acumulan esperando bloqueos; la latencia crece de forma no lineal; sigue la agotamiento del pool.
  • Retraso de replicación: en réplicas asíncronas, transacciones largas y esperas de bloqueo aumentan el tiempo de apply; en semi-sync, pueden enlentecer los commits.

Broma #2: Si quieres que dos equipos se alineen en el orden de bloqueos, diles que es obligatorio y programa una reunión—interbloqueo garantizado.

Telemetría y herramientas: MySQL vs MariaDB bajo presión

La ventaja de MySQL: una historia de instrumentación para gobernarlas todas

Performance Schema de MySQL 8.0 es adonde vas para responder “qué bloquea a qué” sin adivinar. La ventaja operacional clave
es la consistencia: mismas tablas, mismos join, mismo modelo mental entre hosts. Cuando el sitio está en llamas, consistencia es velocidad.

Normalmente puedes obtener:

  • esperadores y bloqueadores actuales (performance_schema.data_lock_waits)
  • inventario de bloqueos (performance_schema.data_locks)
  • metadatos de transacciones (information_schema.innodb_trx)
  • texto SQL y resúmenes de digest (events_statements_current, sumarios digest)

MariaDB: puede ser buena, pero vigila la versión y la ruta del motor

MariaDB también tiene Performance Schema, pero operativamente es más común encontrarlo deshabilitado, parcialmente habilitado o menos utilizado.
MariaDB además recurre a vistas de Information Schema como INFORMATION_SCHEMA.INNODB_LOCK_WAITS en muchas tiendas (cuando está disponible),
y, por supuesto, la vieja caballada: SHOW ENGINE INNODB STATUS.

Si tu estate de MariaDB incluye Galera, necesitas un modelo mental adicional: no toda “transacción abortada” es un interbloqueo de InnoDB.
Algunas son conflictos a nivel de replicación de conjuntos de escritura. La depurabilidad depende de si recoges esas estadísticas y las mapeas al comportamiento de la app.

Diferencias de logging que deciden si encuentras al culpable en minutos u horas

Los logs de interbloqueo valen tanto como tu configuración. En MySQL y MariaDB quieres:

  • registro de interbloqueos habilitado (InnoDB imprime detalles en el error log)
  • timestamps + IDs de hilo alineados con tus logs de aplicación
  • slow log y/o datos de digest de sentencias para conectar el interbloqueo con la forma de la consulta, no solo un snippet SQL aislado

Charla real: un informe de interbloqueo sin identificar la ruta de código es solo una galleta de la fortuna cara.

Guion de diagnóstico rápido

Esta es la secuencia de “dejar de adivinar”. Es intencionalmente corta. Bajo carga, la primera prioridad es identificar:
(1) si tienes una tormenta de interbloqueos o una cola de espera de bloqueo, (2) qué tablas/índices están calientes, (3) qué transacción es el matón.

Primero: confirma qué tipo de dolor tienes

  1. Revisa tasas de error en la app: ¿ves 1213 (interbloqueo) o 1205 (timeout)?
  2. Revisa contadores de estado de InnoDB: ¿aumentan rápido las esperas de bloqueo? ¿crece la longitud de la lista de historial (transacciones largas)?
  3. Confirma salud del thread pool / connection pool: ¿los hilos están atascados en “Waiting for table metadata lock” o en esperas de bloqueo de filas?

Segundo: encuentra al/los bloqueador(es) y los objetos calientes

  1. Lista las esperas de bloqueo e identifica los IDs de transacción que bloquean.
  2. Mapea los bloqueadores al texto SQL y al cliente/usuario/host.
  3. Identifica la tabla e índice involucrados. Si es un escaneo de índice secundario por rango, asume que la ruta de acceso es el bug.

Tercero: elige una acción de contención

  1. Termina la transacción bloqueadora más perjudicial si es seguro (a menudo un job por lotes largo o una consulta de mantenimiento atascada).
  2. Reduce temporalmente la concurrencia del endpoint/tipo de worker ofensivo.
  3. Ajusta el comportamiento de reintento: backoff exponencial con jitter y límite de reintentos. No generes un DDOS de reintentos.

Solo después de la contención haces el trabajo “bonito”: índices, reescritura de consultas, orden de bloqueo, revisión del nivel de aislamiento.

Tareas prácticas: comandos, salidas, decisiones (12+)

El objetivo de estas tareas no es coleccionar trivia. Es convertir misterio en una decisión: “matar esto,” “limitAR aquello,” “reescribir esta consulta,”
“añadir este índice,” o “cambiar este límite de transacción.” Cada tarea incluye: comando, qué significa la salida y qué haces después.

Task 1: confirmar motor/versión y si siquiera estás jugando el mismo juego

cr0x@server:~$ mysql -uroot -p -e "SELECT VERSION() AS version, @@version_comment AS comment, @@innodb_version AS innodb_version\G"
*************************** 1. row ***************************
version: 8.0.36
comment: MySQL Community Server - GPL
innodb_version: 8.0.36

Significado: La versión te dice qué tablas de instrumentación existen y qué valores por defecto aplican.

Decisión: Si estás en MySQL 5.7 / MariaDB antiguo, planea menos opciones de introspección en vivo; confía más en logs y SHOW ENGINE INNODB STATUS.

Task 2: comprobar si tratas con interbloqueos o timeouts a nivel servidor

cr0x@server:~$ mysql -uroot -p -e "SHOW GLOBAL STATUS LIKE 'Innodb_deadlocks'; SHOW GLOBAL STATUS LIKE 'Innodb_row_lock_timeouts';"
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| Innodb_deadlocks| 482   |
+-----------------+-------+
+---------------------------+-------+
| Variable_name             | Value |
+---------------------------+-------+
| Innodb_row_lock_timeouts  | 91    |
+---------------------------+-------+

Significado: Interbloqueos que suben rápidamente apuntan a orden de bloqueo en conflicto; timeouts apuntan a esperas largas y hotspots.

Decisión: Si Innodb_deadlocks está en pico, prioriza identificar las dos formas SQL implicadas. Si los timeouts suben, busca la transacción más larga y los índices faltantes.

Task 3: capturar la narrativa del último interbloqueo (rápido, pero no exhaustivo)

cr0x@server:~$ mysql -uroot -p -e "SHOW ENGINE INNODB STATUS\G" | sed -n '/LATEST DETECTED DEADLOCK/,+80p'
LATEST DETECTED DEADLOCK
------------------------
2025-12-29 10:41:12 0x7f1c4c1fe700
*** (1) TRANSACTION:
TRANSACTION 123456789, ACTIVE 0 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 5 lock struct(s), heap size 1136, 3 row lock(s)
MySQL thread id 9012, OS thread handle 139759, query id 882199 appdb 10.2.3.44 appuser updating
UPDATE orders SET status='paid' WHERE id=778812
*** (2) TRANSACTION:
TRANSACTION 123456790, ACTIVE 0 sec starting index read
mysql tables in use 1, locked 1
5 lock struct(s), heap size 1136, 3 row lock(s)
MySQL thread id 9013, OS thread handle 139760, query id 882200 appdb 10.2.3.45 appuser updating
UPDATE orders SET status='shipped' WHERE id=778812
*** WE ROLL BACK TRANSACTION (1)

Significado: Obtienes el SQL de la víctima y el peer en conflicto, además de detalles de índice/bloqueo más abajo.

Decisión: Si las dos sentencias vienen del mismo camino de código, revisa orden no determinista o lecturas ocultas (chequeos de FK, triggers). Si son de rutas diferentes, alinea el orden de bloqueos o divide el modelo de propiedad de fila.

Task 4 (MySQL 8.0): listar bloqueadores y esperadores actuales vía Performance Schema

cr0x@server:~$ mysql -uroot -p -e "
SELECT
  w.REQUESTING_ENGINE_TRANSACTION_ID AS waiting_trx,
  w.BLOCKING_ENGINE_TRANSACTION_ID  AS blocking_trx,
  dlw.OBJECT_SCHEMA, dlw.OBJECT_NAME, dlw.INDEX_NAME,
  dlw.LOCK_TYPE, dlw.LOCK_MODE, dlw.LOCK_STATUS
FROM performance_schema.data_lock_waits w
JOIN performance_schema.data_locks dlw
  ON w.REQUESTING_ENGINE_LOCK_ID = dlw.ENGINE_LOCK_ID
ORDER BY dlw.OBJECT_SCHEMA, dlw.OBJECT_NAME\G"
*************************** 1. row ***************************
waiting_trx: 123456799
blocking_trx: 123456650
OBJECT_SCHEMA: appdb
OBJECT_NAME: order_items
INDEX_NAME: idx_order_id
LOCK_TYPE: RECORD
LOCK_MODE: X
LOCK_STATUS: WAITING

Significado: Esto apunta a la tabla/índice caliente y al ID de transacción bloqueador.

Decisión: Recupera de inmediato el SQL y la duración de ambas transacciones. Si el bloqueador es “obviamente incorrecto” (batch largo), mátalo. Si es tráfico normal, tienes un problema estructural de contención.

Task 5 (MySQL 8.0): mapear IDs de transacción a sesiones y texto SQL

cr0x@server:~$ mysql -uroot -p -e "
SELECT
  t.trx_id, t.trx_started, TIMESTAMPDIFF(SECOND, t.trx_started, NOW()) AS trx_age_s,
  p.ID AS processlist_id, p.USER, p.HOST, p.DB, p.COMMAND, p.TIME, p.STATE,
  LEFT(p.INFO, 200) AS sql_sample
FROM information_schema.innodb_trx t
JOIN information_schema.PROCESSLIST p
  ON p.ID = t.trx_mysql_thread_id
ORDER BY trx_age_s DESC
LIMIT 10\G"
*************************** 1. row ***************************
trx_id: 123456650
trx_started: 2025-12-29 10:38:01
trx_age_s: 191
processlist_id: 8441
USER: batch
HOST: 10.2.8.19:55312
DB: appdb
COMMAND: Query
TIME: 187
STATE: Updating
sql_sample: UPDATE order_items SET price=price*0.98 WHERE order_id IN (...)

Significado: Has encontrado al matón: una transacción de larga duración que mantiene bloqueos.

Decisión: Si no es crítica, mátala. Si es crítica, limita concurrent writers y reduce la huella de bloqueo (indexado, chunking, predicados más estrechos) tras la contención.

Task 6: inspeccionar la variable innodb_lock_wait_timeout (y dejar de usarla a lo tonto)

cr0x@server:~$ mysql -uroot -p -e "SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';"
+--------------------------+-------+
| Variable_name            | Value |
+--------------------------+-------+
| innodb_lock_wait_timeout | 50    |
+--------------------------+-------+

Significado: Esto es cuánto espera una transacción antes de rendirse. No previene interbloqueos.

Decisión: No “arregles” interbloqueos poniendo esto a 1 o 500. Ajústalo según el presupuesto de latencia visible al usuario y la tolerancia de jobs background, luego arregla el patrón de contención.

Task 7: comprobar el nivel de aislamiento (los interbloqueos son sensibles a ello)

cr0x@server:~$ mysql -uroot -p -e "SELECT @@transaction_isolation AS isolation;"
+--------------+
| isolation    |
+--------------+
| REPEATABLE-READ |
+--------------+

Significado: REPEATABLE READ puede introducir bloqueos next-key/gap para escaneos de rango y búsquedas por índice.

Decisión: Si los interbloqueos involucran predicados de rango (p. ej., WHERE created_at BETWEEN...), considera READ COMMITTED para la carga, pero solo con una revisión explícita de consistencia.

Task 8: detectar acumulación de metadata locks (no son interbloqueos, pero el outage se parece)

cr0x@server:~$ mysql -uroot -p -e "SHOW PROCESSLIST;" | sed -n '1,20p'
Id	User	Host	db	Command	Time	State	Info
8123	appuser	10.2.3.12:51221	appdb	Query	45	Waiting for table metadata lock	ALTER TABLE orders ADD COLUMN x INT
8124	appuser	10.2.3.13:51222	appdb	Query	44	Waiting for table metadata lock	SELECT * FROM orders WHERE id=... 
8125	appuser	10.2.3.14:51223	appdb	Query	44	Waiting for table metadata lock	UPDATE orders SET ...

Significado: DDL está bloqueando lecturas/escrituras vía MDL. Esto no es un interbloqueo de InnoDB; es un incidente de cambio de esquema.

Decisión: Pausa/mata el DDL si es seguro, o muévelo a una estrategia de cambio de esquema online. No pierdas tiempo “depurando interbloqueos” aquí.

Task 9: comprobar longitud grande de history list (presión de undo, transacciones largas)

cr0x@server:~$ mysql -uroot -p -e "SHOW ENGINE INNODB STATUS\G" | sed -n '/History list length/,+3p'
History list length 987654
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 123456650, ACTIVE 191 sec
... 

Significado: Una longitud masiva de history list suele significar transacciones de larga duración que impiden el purge. Se correlaciona con churn de bloqueos y colapso de rendimiento.

Decisión: Encuentra y corrige transacciones largas: jobs por lotes, patrones de “leer en transacción”, o código de app que mantiene transacciones abiertas mientras llama a servicios externos (sí, pasa).

Task 10: confirmar índices usados por las consultas que generan interbloqueo (la ruta de acceso suele ser el bug)

cr0x@server:~$ mysql -uroot -p -e "EXPLAIN UPDATE order_items SET price=price*0.98 WHERE order_id IN (101,102,103)\G"
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: order_items
partitions: NULL
type: range
possible_keys: idx_order_id
key: idx_order_id
key_len: 8
ref: NULL
rows: 45000
filtered: 100.00
Extra: Using where

Significado: Un escaneo por rango que bloquea muchas filas es un contribuyente clásico a interbloqueos.

Decisión: Cambia la forma de la consulta (chunk por primary key, hacer join con una tabla temporal de IDs, o asegurar acceso por igualdad). Si debe tocar muchas filas, serializa ese tipo de carga.

Task 11 (MySQL 8.0): obtener digests de sentencias para detectar principales ofensores de bloqueo

cr0x@server:~$ mysql -uroot -p -e "
SELECT
  DIGEST_TEXT,
  COUNT_STAR,
  SUM_LOCK_TIME/1000000000000 AS sum_lock_time_s,
  SUM_TIMER_WAIT/1000000000000 AS sum_total_time_s
FROM performance_schema.events_statements_summary_by_digest
ORDER BY SUM_LOCK_TIME DESC
LIMIT 5\G"
*************************** 1. row ***************************
DIGEST_TEXT: UPDATE `order_items` SET `price` = `price` * ? WHERE `order_id` IN ( ... )
COUNT_STAR: 11922
sum_lock_time_s: 843.1200
sum_total_time_s: 910.5523

Significado: Esto te da la familia de consultas reincidentes, no una muestra aleatoria.

Decisión: Arregla primero el digest con mayor tiempo agregado de bloqueo. Ahí compras reducción de frecuencia de incidentes.

Task 12: verificar la configuración de logging de interbloqueos

cr0x@server:~$ mysql -uroot -p -e "SHOW VARIABLES LIKE 'innodb_print_all_deadlocks';"
+---------------------------+-------+
| Variable_name             | Value |
+---------------------------+-------+
| innodb_print_all_deadlocks| OFF   |
+---------------------------+-------+

Significado: Si está OFF, solo obtienes el último interbloqueo en la salida de InnoDB status; puedes perder historial bajo condiciones de tormenta.

Decisión: Actívalo en entornos donde la forense de interbloqueos importe y el volumen de logs sea aceptable. Si no puedes permitir el ruido en error log, confía en el historial de Performance Schema si está disponible.

Task 13: identificar y terminar una sesión bloqueadora (quirúrgico, no en pánico)

cr0x@server:~$ mysql -uroot -p -e "KILL 8441;"
Query OK, 0 rows affected (0.00 sec)

Significado: La sesión 8441 se termina; su transacción se revierte y libera bloqueos.

Decisión: Solo haz esto cuando confirmes que es el bloqueador y el coste de rollback es aceptable. Si la transacción modificó millones de filas, matarla puede empeorar I/O y churn de undo temporalmente.

Task 14: comprobar lag de replicación y estado del hilo de apply (los interbloqueos pueden ser síntomas upstream)

cr0x@server:~$ mysql -uroot -p -e "SHOW REPLICA STATUS\G" | sed -n '1,35p'
Replica_IO_Running: Yes
Replica_SQL_Running: Yes
Seconds_Behind_Source: 420
Last_SQL_Error: 
Replica_SQL_Running_State: Waiting for dependent transaction to commit

Significado: El lag sugiere que las transacciones son lentas de aplicar, a menudo debido a esperas de bloqueo o transacciones grandes.

Decisión: Si el lag sube durante la tormenta de interbloqueos, espera lecturas obsoletas en réplicas y considera enrutar menos lecturas ahí, o detener jobs de escritura no esenciales.

Task 15: comprobar saturación a nivel OS (porque el “interbloqueo” puede ser un disco lento fingiendo)

cr0x@server:~$ iostat -x 1 3
Linux 6.1.0 (db01) 	12/29/2025 	_x86_64_	(16 CPU)

avg-cpu:  %user   %nice %system %iowait  %steal   %idle
          12.31    0.00    6.88   34.22    0.00   46.59

Device            r/s     w/s   rkB/s   wkB/s  await  svctm  %util
nvme0n1         120.0   980.0  4096.0 32768.0  48.10   0.85  92.30

Significado: Alto iowait y alto await indican latencia de almacenamiento. I/O lento alarga el tiempo de transacción, aumentando la duración de los bloqueos y la probabilidad de interbloqueos.

Decisión: Si el almacenamiento es el cuello de botella, tu “problema de interbloqueos” es en parte un problema de infraestructura: reduce la amplificación de escrituras (batching, revisión de índices) y arregla la contención de I/O subyacente.

Tres microhistorias del mundo corporativo

Incidente #1: el outage causado por una suposición equivocada

Una plataforma de comercio mediana migró de un despliegue legacy de MySQL a un cluster MariaDB porque “es básicamente lo mismo y nos gusta la gobernanza open source.”
La migración fue bien. Las pruebas de rendimiento parecían aceptables. Todos celebraron de forma responsable programando el corte para una mañana entre semana.

La suposición equivocada fue sutil: el equipo asumió que su lógica de reintentos—escrita años antes—manejaba interbloqueos “robustamente” porque reintentaba sobre
errores genéricos de base de datos. Reintentaba. Inmediatamente. Sin backoff. Y reintentaba transacciones que no eran idempotentes cuando ya se había hecho trabajo parcial
en la capa de aplicación.

En el día del corte, se toparon con un patrón de conflicto de escrituras que no habían visto antes. Un par de filas calientes (contadores de inventario y filas de estado de pedido) se convirtieron en imanes de contención.
La base de datos hizo lo que hacen las bases de datos: detectar interbloqueos y abortar víctimas. La app hizo lo que hacen las apps mal supervisadas: reintentar todo, rápido, en paralelo, sin considerar efectos visibles al usuario.

El sitio no se cayó instantáneamente. Se volvió más lento. Luego las colas de workers se saturaron. Luego se llenó el pool de conexiones. Luego las comprobaciones de salud empezaron a fallar.
Desde fuera parecía “la base de datos está interbloqueando constantemente.” Desde dentro era “la aplicación amplifica un mecanismo de seguridad normal en un denial of service.”

La solución no fue “volver a MySQL.” La solución fue ingeniería aburrida: hacer reintentos condicionales, usar backoff exponencial con jitter, limitar reintentos,
y hacer la transacción realmente idempotente (o diseñar una acción compensadora). También introdujeron un presupuesto de interbloqueos por endpoint: si se excede,
el endpoint se limita antes de que la base de datos sea forzada a hacerlo.

Incidente #2: la optimización que salió mal

Una compañía SaaS de analítica tenía un job de generación de reportes lento. Actualizaba una tabla “report_status” para millones de usuarios.
Alguien vio la ineficiencia clásica: “Hacemos demasiados commits. Envolvámos todo el job en una sola transacción para velocidad.”
En una base de staging tranquila, parecía brillante. Una transacción, un commit, menos overhead. Los gráficos asintieron educadamente.

En producción, esa “optimización” se volvió un monstruo que mantenía bloqueos. La transacción corría durante minutos, a veces más.
Durante ese tiempo mantenía bloqueos en entradas de índices secundarios y causó que otros workflows—especialmente actualizaciones orientadas al usuario—se pusieran en cola detrás.
Aumentaron los interbloqueos porque ahora cualquier otra transacción tenía más tiempo para colisionar con el corredor largo.

El patrón del incidente fue desagradable: no un único fallo dramático, sino una espiral de latencia creciente. Las peticiones de clientes se ralentizaron.
Las tareas background reintentaron. El lag de replicación creció. Los operadores mataron el job; el rollback llevó tiempo; el sistema se sintió peor antes de mejorar.
El equipo intentó entonces la peor “solución”: subir innodb_lock_wait_timeout. Ahora las transacciones esperaban más para fallar, así que la concurrencia se mantenía alta mientras el sistema hacía menos progreso.

La solución real fue operativamente poco sexy y técnicamente correcta: fragmentar el job en pequeños lotes deterministas, commitear cada N filas,
y ordenar actualizaciones por primary key para mantener adquisición de bloqueos consistente. Aceptaron un poco más de overhead de commit a cambio de vidas de bloqueo predecibles.
El job quedó algo más lento en aislamiento y dramáticamente más rápido para el negocio porque el sitio se mantuvo responsive.

La lección: cualquier optimización que aumente la duración de la transacción es una optimización en contra de tu propia disponibilidad.

Incidente #3: la práctica aburrida que salvó el día

Un equipo fintech ejecutaba MySQL 8.0 con higiene operativa estricta. Nada exótico: Performance Schema habilitado, logs de error enviados y buscables,
dashboards para esperas de bloqueo y un “paquete de consultas de incidente” estándar en un repo compartido. Cada on-call había corrido el paquete en un game day al menos una vez.

Una tarde, una release inofensiva causó un aumento súbito de interbloqueos en una tabla que rastreaba el estado de verificación de usuarios.
Los clientes no podían completar onboarding. Las entradas de soporte empezaron a apilarse. El on-call entró.

No empezaron debatiendo niveles de aislamiento ni culpando al ORM. Ejecutaron la consulta de lock-wait contra Performance Schema, identificaron el digest bloqueador,
y vieron inmediatamente una nueva forma de consulta: un UPDATE con un predicado de rango en una columna de timestamp, con un índice faltante, ejecutado dentro de una transacción que además leía de otra tabla.
Esa consulta no solo era lenta; estaba bloqueando rangos.

La contención fue simple: desactivar con feature flag ese camino de código y limitar un worker background que golpeaba la misma tabla.
Con la presión reducida, añadieron el índice compuesto faltante y reescribieron la transacción para adquirir bloqueos en orden consistente.
El incidente terminó rápido, y el postmortem fue refrescantemente corto.

La práctica que los salvó no fue heroica. Fue instrumentación rutinaria, runbooks rutinarios y la disciplina de mantener el alcance de las transacciones pequeño.
Lo aburrido es bueno. Lo aburrido escala.

Prevenir interbloqueos sin engañarse a uno mismo

1) Trata los interbloqueos como una condición manejada, no como una excepción sorpresa

Los interbloqueos ocurrirán en cualquier sistema suficientemente concurrente. Tu trabajo es asegurarte de que sean:
(a) raros, (b) baratos y (c) inofensivos. Eso significa que los reintentos deben ser seguros.
Si no puedes reintentar de forma segura, tu modelo de datos y los límites de transacción son incorrectos para la concurrencia.

2) Mantén las transacciones cortas, deterministas y locales

El amplificador de interbloqueos más común es el tiempo. Cada milisegundo que mantienes bloqueos es otro boleto de lotería para colisión.
Reduce el alcance de la transacción. No mantengas una transacción abierta mientras:

  • llamas a servicios externos
  • renderizas plantillas
  • esperas en una cola
  • realizas lecturas no indexadas que “planeas actualizar después”

3) Usa un orden de bloqueo consistente entre rutas de código

Si dos transacciones tocan el mismo conjunto de filas pero las bloquean en órdenes diferentes, tienes una fábrica de interbloqueos.
Arreglar esto suele ser más barato que intentar “sobreindexar” el problema. Patrones comunes:

  • Siempre actualizar el padre antes del hijo (o al revés), pero elige uno y aplícalo.
  • Al actualizar múltiples filas, ordena los IDs y actualiza en ese orden.
  • Prefiere actualizaciones de una sola fila por primary key cuando sea posible.

4) Diseña para evitar hotspots

Algunas tablas nacen calientes: contadores, filas de “estado actual”, leaderboards, stores de sesión.
Los hotspots causan tanto interbloqueos como timeouts porque concentran escrituras.
Opciones:

  • particiona por clave (incluso dentro de una BD vía partitioning o bucketing a nivel de aplicación)
  • usa tablas append-only de eventos y calcula agregados de forma asíncrona
  • evita contadores “globales”; usa contadores por entidad y consolida

5) No “arregles” interbloqueos bajando el aislamiento como reflejo

Cambiar a READ COMMITTED puede reducir algunos interbloqueos relacionados con gap-locks, sí. También puede cambiar la semántica de tu aplicación.
Si la lógica del negocio asume lecturas consistentes dentro de una transacción, acabas de comprar una nueva clase de bugs.
Hazlo intencionalmente, con revisión de corrección y tests.

Errores comunes: síntoma → causa raíz → solución

1) Síntoma: los interbloqueos aumentan justo después de un despliegue

Causa raíz: nueva forma de consulta toca filas en diferente orden, o añade un escaneo por rango por índice faltante.

Solución: compara digests de sentencias/tiempo de bloqueo antes y después; revierte o feature-flag; añade/ajusta índices; aplica orden de bloqueos en código.

2) Síntoma: muchas sesiones “Waiting for table metadata lock”

Causa raíz: DDL bloqueando vía MDL, no un interbloqueo de InnoDB.

Solución: para/mata el DDL si es seguro; usa métodos de cambio de esquema online; programa DDL con guardrails y monitorización de MDL.

3) Síntoma: timeouts de espera de bloqueo, pero pocos errores de interbloqueo

Causa raíz: transacción larga o job por lotes que mantiene bloqueos; filas calientes; latencia de almacenamiento alargando transacciones.

Solución: identifica la transacción más larga, fragmenta el job, añade índices faltantes, reduce contención de I/O y asegura que la app no mantenga transacciones abiertas innecesariamente.

4) Síntoma: interbloqueos “aleatorios” bajo carga

Causa raíz: orden de bloqueo no determinista (listas IN sin ordenar, workers paralelos actualizando conjuntos solapados), o cascadas de foreign key bloqueando filas adicionales.

Solución: ordena claves antes de actualizar; serializa workers por clave de partición; revisa cascadas de foreign key y triggers; considera SELECT … FOR UPDATE con orden explícito.

5) Síntoma: subir innodb_lock_wait_timeout empeora las cosas

Causa raíz: aumentaste el tiempo que los hilos esperan, consumiendo concurrencia y memoria, sin aumentar el throughput.

Solución: bájalo para que coincida con tu presupuesto de latencia; arregla el bloqueador; reduce la contención; implementa backpressure en la capa de aplicación.

6) Síntoma: los interbloqueos desaparecen después de “añadir un índice”, pero el rendimiento empeora

Causa raíz: el índice redujo la contención pero aumentó la amplificación de escrituras; creaste un índice caliente nuevo o hiciste las actualizaciones más pesadas.

Solución: valida el coste de escritura; elimina índices no usados; considera índices compuestos que coincidan con los patrones de acceso; mantén el set mínimo que soporte lecturas/escrituras críticas.

7) Síntoma: en despliegues Galera, abortos “tipo interbloqueo” sin trazas claras de InnoDB

Causa raíz: conflictos de certificación de replicación (dos nodos confirman escrituras conflictivas).

Solución: reduce la superposición de escrituras multi-master; enruta escrituras de una entidad a un nodo; revisa la lógica de reintentos y la monitorización de tasa de conflictos.

Listas de verificación / plan paso a paso

Durante el incidente (primeros 15 minutos)

  1. Clasifica el error: 1213 interbloqueo vs 1205 timeout vs MDL. No los mezcles.
  2. Captura evidencia rápido: extracto de InnoDB status, consulta top de lock-wait y las sesiones peores ofensoras.
  3. Contén: mata el bloqueador más grande si es seguro; limita workers/endpoints ofensivos; desactiva la nueva feature si está correlacionada.
  4. Estabiliza reintentos: limita reintentos, añade backoff y deja de reintentar workflows no idempotentes.
  5. Revisa restricciones de infra: latencia de almacenamiento, saturación de CPU, presión de buffer pool. Los interbloqueos son más fáciles de provocar en un sistema lento.

Tras la contención (mismo día)

  1. Identifica los digests ofensores (no consultas aisladas). Arregla al mayor contribuidor.
  2. Reduce el alcance de transacciones y elimina patrones de “transacción mientras se hace trabajo”.
  3. Aplica orden de bloqueos en código y tests (sí, tests).
  4. Revisión de índices: asegura que los predicados coincidan con índices; evita escaneos por rango no intencionados.
  5. Valida el coste de rollback: si matas bloqueadores rutinariamente, entiende el churn de undo y el comportamiento de I/O.

Endurecimiento (así evitas pages repetidos)

  1. Habilita observabilidad durable de interbloqueos: logs enviados, Performance Schema configurado, dashboards para esperas de bloqueo y edad de transacciones.
  2. Crea un presupuesto de interbloqueos por servicio: si excede umbral, auto-limita o rechaza carga.
  3. Game day: practica usando las consultas y comandos exactos arriba bajo un escenario sintético de contención.
  4. Revisa patrones de esquema: hotspots, contadores, máquinas de estado. Rediseña donde sea necesario.

Preguntas frecuentes

1) ¿Es un interbloqueo un bug de la base de datos?

Normalmente no. Es la base de datos haciendo lo correcto cuando dos transacciones crean un ciclo. El bug suele estar en el diseño de concurrencia o manejo de reintentos.

2) ¿Cuál es más fácil de depurar: MySQL o MariaDB?

Con valores por defecto modernos y uso maduro de Performance Schema, MySQL 8.0 tiende a ser más fácil de depurar rápidamente. MariaDB puede ser igualmente manejable, pero la observabilidad varía más según el despliegue.

3) ¿Debería simplemente habilitar innodb_print_all_deadlocks?

Si puedes manejar el volumen de logs y necesitas historial forense, sí. Pero no lo trates como tu única herramienta. Bajo alto churn, los logs pueden volverse ruido y aún así perder el patrón mayor.

4) ¿Por qué ocurren interbloqueos si solo actualizamos por primary key?

Porque los índices secundarios y las comprobaciones de claves foráneas aún pueden introducir bloqueos adicionales. Además, “por primary key” a veces no es realmente por primary key una vez que el ORM interviene.

5) ¿Cuál es la diferencia entre interbloqueo y timeout de espera para la app?

Los errores de interbloqueo son abortos inmediatos para romper un ciclo; los timeouts son el sistema rindiéndose tras esperar. Ambos deben manejarse, pero los timeouts suelen indicar un bloqueador largo o índices faltantes.

6) ¿READ COMMITTED arregla los interbloqueos?

Puede reducir interbloqueos relacionados con gap locks, pero no “arregla” la categoría de interbloqueos. También cambia lo que tus transacciones pueden asumir. Trátalo como un cambio de ingeniería, no como un interruptor.

7) ¿Puedo prevenir interbloqueos aumentando innodb_lock_wait_timeout?

No. Eso afecta timeouts, no la detección de interbloqueos. Aumentarlo a menudo empeora los outages al dejar hilos esperando más tiempo y acumularse.

8) ¿Cuál es la mitigación inmediata más segura durante una tormenta?

Limita la concurrencia del camino de código ofensivo y mata la transacción bloqueadora más grave si el coste del rollback es aceptable. Luego arregla la forma de la consulta y el alcance de la transacción.

9) ¿Cómo sé si es un problema de cambio de esquema en vez de otro?

Si las sesiones muestran “Waiting for table metadata lock”, eso es MDL. Las herramientas de interbloqueo no ayudarán; necesitas gestionar la estrategia de ejecución de DDL.

Conclusión: próximos pasos que realmente reducen los pages

Si quieres menos incidentes de interbloqueos, deja de tratarlos como lore espeluznante de bases de datos y empieza a tratarlos como
un coste de concurrencia observable y clasificable. MySQL 8.0 facilita eso desde el primer momento con Performance Schema.
MariaDB también puede hacerlo, pero necesitarás ser más estricto habilitando y estandarizando la telemetría correcta—especialmente en topologías mixtas.

Pasos prácticos:

  1. Construye un runbook de dos rutas: interbloqueo (1213) vs timeout (1205) vs MDL. Diferentes herramientas, diferentes soluciones.
  2. Habilita y verifica tu observabilidad: logging de interbloqueos, tablas de Performance Schema que realmente consultas, y logs indexables.
  3. Arregla el digest ofensivo principal por tiempo de bloqueo agregado, no por la consulta aislada más llamativa que veas.
  4. Haz los reintentos sensatos: transacciones idempotentes, backoff con jitter y límites. Evita tormentas de reintentos.
  5. Acorta las transacciones y aplica orden de bloqueos. Ese es el trabajo. Esa también es la victoria.
← Anterior
Plan híbrido ZFS: datos en HDD + metadatos en SSD + caché NVMe, bien hecho
Siguiente →
Licenciamiento de VMware ESXi en 2026: qué cambió, cuánto cuesta y las mejores alternativas (incluye Proxmox)

Deja un comentario