Contenido
Si alguna vez has visto un sistema transaccional congelarse bajo una carga pesada, conoces el tipo de pánico silencioso que se instala. Las consultas se acumulan, el uso de la CPU parece normal, los registros parecen normales, pero todo está atascado. Los bloqueos mutuos son problemas sigilosos que no se muestran hasta que causan un daño real. No estropean los datos, pero matan el tiempo de actividad y sacuden la confianza en el sistema, obligando a los equipos a apresurarse para adelantarse a ellos antes de que las cosas empeoren. Un bloqueo mutuo ocurre cuando dos o más transacciones esperan los bloqueos de los demás en un ciclo. El sistema reconoce este ciclo y termina una transacción para romperlo. En sistemas que manejan toneladas de transacciones en línea, una transacción terminada a menudo conduce a reintentos, tiempos de espera y una cascada de fallos vistos por los usuarios. Por eso prevenir los bloqueos mutuos no es solo algo deseable para el rendimiento; es crucial para la resiliencia del sistema.\n\nLos expertos que diseñan sistemas de alto rendimiento coinciden en que los bloqueos mutuos generalmente no son aleatorios. Martin Kleppmann señala que las inconsistencias sutiles en el orden en que se adquieren los bloqueos causan la mayoría de los bloqueos mutuos. Pat Helland argumenta que los sistemas que tratan el orden y la idempotencia como preocupaciones de primera clase rara vez enfrentan bloqueos persistentes. Tammy Butow añade que muchas empresas subestiman cómo las tormentas de reintentos causadas por malas estrategias de retroceso empeoran los bloqueos mutuos. Su consejo se reduce a tres cosas: orden predecible de bloqueos, reintentos limitados y una comprensión clara de cómo se conceden los bloqueos.\n\nEntonces, ¿por qué ocurren los bloqueos mutuos? No son accidentes. Cuatro condiciones suelen ocurrir juntas: exclusión mutua (solo una transacción puede tener un bloqueo a la vez), retención y espera (las transacciones mantienen bloqueos mientras esperan otros), no preempción (los bloqueos no pueden ser arrebatados por la fuerza) y espera circular (cada transacción espera un bloqueo mantenido por otra en un ciclo). Las bases de datos relacionales tienen todo esto por diseño. Los desarrolladores pueden controlar principalmente las esperas circulares. Las causas comunes incluyen órdenes inconsistentes de bloqueo, transacciones de larga duración, uso de niveles de aislamiento demasiado estrictos y patrones de acceso adversos durante picos de tráfico. Por ejemplo, si una transacción actualiza las filas 1 y luego 2, y otra hace 2 y luego 1, pueden bloquearse cuando se superponen de forma incorrecta.\n\nEl daño real de los bloqueos mutuos no es la única transacción terminada; es la tormenta de reintentos que sigue. Los clientes reintentan inmediatamente sin ningún retraso o aleatoriedad, causando que los reintentos choquen una y otra vez. Esto puede convertirse en apagones que duran minutos. Un caso fue una API de pagos donde un bloqueo mutuo de 80 ms causó un apagón parcial de cinco minutos porque los reintentos chocaban fuertemente. A gran escala, incluso un pequeño porcentaje de bloqueos mutuos puede causar una avalancha de fallos que saturan los grupos de conexiones. Es mucho más fácil evitar que ocurra el bloqueo que contener la tormenta después.\n\nPara prevenir bloqueos mutuos, comienza haciendo cumplir un orden estricto y documentado de bloqueos. Cada tipo de transacción debe adquirir bloqueos en el mismo orden, ya sea por clave primaria, tipo de recurso o jerarquía. No permitas que la entrada del usuario o condiciones dinámicas cambien ese orden sobre la marcha, o invitarás ciclos. Si las escrituras condicionales son necesarias, haz primero una lectura simple de metadatos para elegir la ruta ordenada correcta.\n\nMantener las transacciones cortas también ayuda. Las transacciones largas no causan bloqueos directamente, pero aumentan la ventana para conflictos. Elimina consultas innecesarias y operaciones costosas como llamadas a API dentro de las transacciones. Trata la atomicidad como un bisturí en lugar de un gran martillo: solo protege lo que necesita garantías transaccionales.\n\nUsa el nivel de aislamiento correcto en lugar del más fuerte. El aislamiento serializable puede parecer más seguro, pero a menudo significa más bloqueos y mayor riesgo de bloqueo mutuo. Elige el nivel de aislamiento más débil que aún proteja la corrección, como aislamiento de instantánea para lecturas o lectura confirmada para escrituras con verificaciones explícitas. Un aislamiento más alto no siempre es mejor, solo más restrictivo.\n\nIncluso con todo eso, algunos bloqueos mutuos ocurrirán. Así que siempre añade retroceso exponencial con jitter a los bucles de reintento. No reintentes inmediatamente después de un fallo. Aleatoriza los retrasos para que los reintentos no se agrupen y choquen de nuevo. Incluso un pequeño jitter reduce drásticamente las colisiones de reintentos, a menudo salvándote de pasar de un pequeño contratiempo a un incidente grave.\n\nPor último, monitorea la contención de bloqueos como una métrica clave. La mayoría de los equipos vigilan la latencia general de consultas pero ignoran cuánto tiempo esperan las consultas por bloqueos. Medir el tiempo de bloqueo, el conteo de bloqueos mutuos y los principales esperadores de bloqueo ayuda a detectar problemas temprano antes de que causen apagones.