Al trabajar con bases de datos, el control de concurrencia es el concepto que garantiza que las transacciones de la base de datos se realicen simultáneamente sin violar la integridad de los datos.
Hay mucha teoría y diferentes enfoques en torno a este concepto y cómo lograrlo, pero nos referiremos brevemente a la forma en que PostgreSQL y MySQL (cuando se usa InnoDB) lo manejan, y un problema común que puede surgir en sistemas altamente concurrentes:interbloqueos.
Estos motores implementan el control de concurrencia mediante un método denominado MVCC (Control de concurrencia multiversión). En este método, cuando se actualiza un elemento, los cambios no sobrescribirán los datos originales, sino que se creará una nueva versión del elemento (con los cambios). Así tendremos almacenadas varias versiones del ítem.
Una de las principales ventajas de este modelo es que los bloqueos adquiridos para consultar (leer) datos no entran en conflicto con los bloqueos adquiridos para escribir datos, por lo que la lectura nunca bloquea la escritura y la escritura nunca bloquea la lectura.
Pero, si se almacenan varias versiones del mismo artículo, ¿qué versión verá una transacción? Para responder a esa pregunta, debemos revisar el concepto de aislamiento de transacciones. Las transacciones especifican un nivel de aislamiento, que define el grado en que una transacción debe aislarse de las modificaciones de recursos o datos realizadas por otras transacciones. Este grado está directamente relacionado con el bloqueo generado por una transacción, por lo que, como se puede especificar a nivel de transacción, puede determinar el impacto que una transacción en ejecución puede tener sobre otras transacciones en ejecución.
Este es un tema muy interesante y extenso, aunque no entraremos en demasiados detalles en este blog. Recomendamos la documentación oficial de PostgreSQL y MySQL para leer más sobre este tema.
Entonces, ¿por qué estamos entrando en los temas anteriores cuando se trata de interbloqueos? Debido a que los comandos sql adquirirán bloqueos automáticamente para garantizar el comportamiento de MVCC, y el tipo de bloqueo adquirido depende del aislamiento de transacciones definido.
Hay varios tipos de bloqueos (nuevamente, otro tema largo e interesante para revisar para PostgreSQL y MySQL), pero lo importante de ellos es cómo interactúan (más exactamente, cómo entran en conflicto) entre sí. ¿Porqué es eso? Porque dos transacciones no pueden contener bloqueos de modos en conflicto en el mismo objeto al mismo tiempo. Y un detalle no menor, una vez adquirido, normalmente se mantiene un bloqueo hasta el final de la transacción.
Este es un ejemplo de PostgreSQL de cómo los tipos de bloqueo entran en conflicto entre sí:
Conflicto de tipos de bloqueo de PostgreSQLY para MySQL:
Conflicto de tipos de bloqueo de MySQLX=bloqueo exclusivo IX=intención de bloqueo exclusivo
S=bloqueo compartido IS=intención de bloqueo compartido
Entonces, ¿qué sucede cuando tengo dos transacciones en ejecución que desean mantener bloqueos en conflicto en el mismo objeto al mismo tiempo? Uno de ellos obtendrá el candado y el otro tendrá que esperar.
Así que ahora estamos en condiciones de comprender verdaderamente lo que sucede durante un punto muerto.
¿Qué es un interbloqueo entonces? Como puede imaginar, hay varias definiciones para un bloqueo de base de datos, pero me gusta la siguiente por su simplicidad.
Un interbloqueo de la base de datos es una situación en la que dos o más transacciones están esperando que la otra abandone los bloqueos.
Entonces, por ejemplo, la siguiente situación nos llevará a un punto muerto:
Ejemplo de punto muertoAquí, la aplicación A obtiene un bloqueo en la fila 1 de la tabla 1 para realizar una actualización.
Al mismo tiempo, la aplicación B obtiene un bloqueo en la fila 2 de la tabla 2.
Ahora, la aplicación A necesita obtener un bloqueo en la tabla 2, fila 2, para continuar con la ejecución y finalizar la transacción, pero no puede obtener el bloqueo porque está retenido por la aplicación B. La aplicación A debe esperar a que la aplicación B la libere. .
Pero la aplicación B necesita obtener un bloqueo en la fila 1 de la tabla 1 para continuar con la ejecución y finalizar la transacción, pero no puede obtener el bloqueo porque está en manos de la aplicación A.
Así que aquí estamos en una situación de punto muerto. La aplicación A está esperando el recurso que tiene la aplicación B para finalizar y la aplicación B está esperando el recurso que tiene la aplicación A. Entonces, ¿cómo continuar? El motor de la base de datos detectará el interbloqueo y eliminará una de las transacciones, desbloqueará la otra y generará un error de interbloqueo en la eliminada.
Veamos algunos ejemplos de interbloqueo de PostgreSQL y MySQL:
PostgreSQL
Supongamos que tenemos una base de datos de prueba con información de los países del mundo.
world=# SELECT code,region,population FROM country WHERE code IN ('NLD','AUS');
code | region | population
------+---------------------------+------------
NLD | Western Europe | 15864000
AUS | Australia and New Zealand | 18886000
(2 rows)
Tenemos dos sesiones que quieren hacer cambios en la base de datos.
La primera sesión modificará el campo de región para el código NLD y el campo de población para el código AUS.
La segunda sesión modificará el campo de región para el código AUS y el campo de población para el código NLD.
Datos de la tabla:
code: NLD
region: Western Europe
population: 15864000
code: AUS
region: Australia and New Zealand
population: 18886000
Sesión 1:
world=# BEGIN;
BEGIN
world=# UPDATE country SET region='Europe' WHERE code='NLD';
UPDATE 1
Sesión 2:
world=# BEGIN;
BEGIN
world=# UPDATE country SET region='Oceania' WHERE code='AUS';
UPDATE 1
world=# UPDATE country SET population=15864001 WHERE code='NLD';
La sesión 2 se colgará esperando que finalice la sesión 1.
Sesión 1:
world=# UPDATE country SET population=18886001 WHERE code='AUS';
ERROR: deadlock detected
DETAIL: Process 1181 waits for ShareLock on transaction 579; blocked by process 1148.
Process 1148 waits for ShareLock on transaction 578; blocked by process 1181.
HINT: See server log for query details.
CONTEXT: while updating tuple (0,15) in relation "country"
Aquí tenemos nuestro punto muerto. El sistema detectó el interbloqueo y eliminó la sesión 1.
Sesión 2:
world=# BEGIN;
BEGIN
world=# UPDATE country SET region='Oceania' WHERE code='AUS';
UPDATE 1
world=# UPDATE country SET population=15864001 WHERE code='NLD';
UPDATE 1
Y podemos verificar que la segunda sesión finalizó correctamente después de que se detectó el interbloqueo y se eliminó la sesión 1 (por lo tanto, se liberó el bloqueo).
Para tener más detalles podemos ver el log en nuestro servidor PostgreSQL:
2018-05-16 12:56:38.520 -03 [1181] ERROR: deadlock detected
2018-05-16 12:56:38.520 -03 [1181] DETAIL: Process 1181 waits for ShareLock on transaction 579; blocked by process 1148.
Process 1148 waits for ShareLock on transaction 578; blocked by process 1181.
Process 1181: UPDATE country SET population=18886001 WHERE code='AUS';
Process 1148: UPDATE country SET population=15864001 WHERE code='NLD';
2018-05-16 12:56:38.520 -03 [1181] HINT: See server log for query details.
2018-05-16 12:56:38.520 -03 [1181] CONTEXT: while updating tuple (0,15) in relation "country"
2018-05-16 12:56:38.520 -03 [1181] STATEMENT: UPDATE country SET population=18886001 WHERE code='AUS';
2018-05-16 12:59:50.568 -03 [1181] ERROR: current transaction is aborted, commands ignored until end of transaction block
Aquí podremos ver los comandos reales que se detectaron en interbloqueo.
Descargue el documento técnico hoy Administración y automatización de PostgreSQL con ClusterControlObtenga información sobre lo que necesita saber para implementar, monitorear, administrar y escalar PostgreSQLDescargar el documento técnicoMySQL
Para simular un interbloqueo en MySQL podemos hacer lo siguiente.
Al igual que con PostgreSQL, supongamos que tenemos una base de datos de prueba con información sobre actores y películas, entre otras cosas.
mysql> SELECT first_name,last_name FROM actor WHERE actor_id IN (1,7);
+------------+-----------+
| first_name | last_name |
+------------+-----------+
| PENELOPE | GUINESS |
| GRACE | MOSTEL |
+------------+-----------+
2 rows in set (0.00 sec)
Tenemos dos procesos que quieren hacer cambios en la base de datos.
El primer proceso modificará el campo first_name para actor_id 1 y el campo last_name para actor_id 7.
El segundo proceso modificará el campo first_name para actor_id 7 y el campo last_name para actor_id 1.
Datos de la tabla:
actor_id: 1
first_name: PENELOPE
last_name: GUINESS
actor_id: 7
first_name: GRACE
last_name: MOSTEL
Sesión 1:
mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)
mysql> UPDATE actor SET first_name='GUINESS' WHERE actor_id='1';
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
Sesión 2:
mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)
mysql> UPDATE actor SET first_name='MOSTEL' WHERE actor_id='7';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> UPDATE actor SET last_name='PENELOPE' WHERE actor_id='1';
La sesión 2 se colgará esperando que finalice la sesión 1.
Sesión 1:
mysql> UPDATE actor SET last_name='GRACE' WHERE actor_id='7';
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
Aquí tenemos nuestro punto muerto. El sistema detectó el interbloqueo y eliminó la sesión 1.
Sesión 2:
mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)
mysql> UPDATE actor SET first_name='MOSTEL' WHERE actor_id='7';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> UPDATE actor SET last_name='PENELOPE' WHERE actor_id='1';
Query OK, 1 row affected (8.52 sec)
Rows matched: 1 Changed: 1 Warnings: 0
Como podemos ver en el error, como vimos para PostgreSQL, hay un interbloqueo entre ambos procesos.
Para más detalles podemos usar el comando SHOW ENGINE INNODB STATUS\G:
mysql> SHOW ENGINE INNODB STATUS\G
------------------------
LATEST DETECTED DEADLOCK
------------------------
2018-05-16 18:55:46 0x7f4c34128700
*** (1) TRANSACTION:
TRANSACTION 1456, ACTIVE 33 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 54, OS thread handle 139965388506880, query id 15876 localhost root updating
UPDATE actor SET last_name='PENELOPE' WHERE actor_id='1'
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 23 page no 3 n bits 272 index PRIMARY of table `sakila`.`actor` trx id 1456 lock_mode X locks rec but not gap waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 2; hex 0001; asc ;;
1: len 6; hex 0000000005af; asc ;;
2: len 7; hex 2d000001690110; asc - i ;;
3: len 7; hex 4755494e455353; asc GUINESS;;
4: len 7; hex 4755494e455353; asc GUINESS;;
5: len 4; hex 5afca8b3; asc Z ;;
*** (2) TRANSACTION:
TRANSACTION 1455, ACTIVE 47 sec starting index read, thread declared inside InnoDB 5000
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 53, OS thread handle 139965267871488, query id 16013 localhost root updating
UPDATE actor SET last_name='GRACE' WHERE actor_id='7'
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 23 page no 3 n bits 272 index PRIMARY of table `sakila`.`actor` trx id 1455 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 2; hex 0001; asc ;;
1: len 6; hex 0000000005af; asc ;;
2: len 7; hex 2d000001690110; asc - i ;;
3: len 7; hex 4755494e455353; asc GUINESS;;
4: len 7; hex 4755494e455353; asc GUINESS;;
5: len 4; hex 5afca8b3; asc Z ;;
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 23 page no 3 n bits 272 index PRIMARY of table `sakila`.`actor` trx id 1455 lock_mode X locks rec but not gap waiting
Record lock, heap no 202 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 2; hex 0007; asc ;;
1: len 6; hex 0000000005b0; asc ;;
2: len 7; hex 2e0000016a0110; asc . j ;;
3: len 6; hex 4d4f5354454c; asc MOSTEL;;
4: len 6; hex 4d4f5354454c; asc MOSTEL;;
5: len 4; hex 5afca8c1; asc Z ;;
*** WE ROLL BACK TRANSACTION (2)
Bajo el título "ÚLTIMO MUERTE DETECTADO", podemos ver los detalles de nuestro punto muerto.
Para ver el detalle del punto muerto en el registro de errores de mysql, debemos habilitar la opción innodb_print_all_deadlocks en nuestra base de datos.
mysql> set global innodb_print_all_deadlocks=1;
Query OK, 0 rows affected (0.00 sec)
Error de registro de MySQL:
2018-05-17T18:36:58.341835Z 12 [Note] InnoDB: Transactions deadlock detected, dumping detailed information.
2018-05-17T18:36:58.341869Z 12 [Note] InnoDB:
*** (1) TRANSACTION:
TRANSACTION 1812, ACTIVE 42 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 11, OS thread handle 140515492943616, query id 8467 localhost root updating
UPDATE actor SET last_name='PENELOPE' WHERE actor_id='1'
2018-05-17T18:36:58.341945Z 12 [Note] InnoDB: *** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 23 page no 3 n bits 272 index PRIMARY of table `sakila`.`actor` trx id 1812 lock_mode X locks rec but not gap waiting
Record lock, heap no 204 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 2; hex 0001; asc ;;
1: len 6; hex 000000000713; asc ;;
2: len 7; hex 330000016b0110; asc 3 k ;;
3: len 7; hex 4755494e455353; asc GUINESS;;
4: len 7; hex 4755494e455353; asc GUINESS;;
5: len 4; hex 5afdcb89; asc Z ;;
2018-05-17T18:36:58.342347Z 12 [Note] InnoDB: *** (2) TRANSACTION:
TRANSACTION 1811, ACTIVE 65 sec starting index read, thread declared inside InnoDB 5000
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 12, OS thread handle 140515492677376, query id 9075 localhost root updating
UPDATE actor SET last_name='GRACE' WHERE actor_id='7'
2018-05-17T18:36:58.342409Z 12 [Note] InnoDB: *** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 23 page no 3 n bits 272 index PRIMARY of table `sakila`.`actor` trx id 1811 lock_mode X locks rec but not gap
Record lock, heap no 204 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 2; hex 0001; asc ;;
1: len 6; hex 000000000713; asc ;;
2: len 7; hex 330000016b0110; asc 3 k ;;
3: len 7; hex 4755494e455353; asc GUINESS;;
4: len 7; hex 4755494e455353; asc GUINESS;;
5: len 4; hex 5afdcb89; asc Z ;;
2018-05-17T18:36:58.342793Z 12 [Note] InnoDB: *** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 23 page no 3 n bits 272 index PRIMARY of table `sakila`.`actor` trx id 1811 lock_mode X locks rec but not gap waiting
Record lock, heap no 205 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 2; hex 0007; asc ;;
1: len 6; hex 000000000714; asc ;;
2: len 7; hex 340000016c0110; asc 4 l ;;
3: len 6; hex 4d4f5354454c; asc MOSTEL;;
4: len 6; hex 4d4f5354454c; asc MOSTEL;;
5: len 4; hex 5afdcba0; asc Z ;;
2018-05-17T18:36:58.343105Z 12 [Note] InnoDB: *** WE ROLL BACK TRANSACTION (2)
Teniendo en cuenta lo que hemos aprendido anteriormente sobre por qué ocurren los interbloqueos, puede ver que no hay mucho que podamos hacer en el lado de la base de datos para evitarlos. De todos modos, como DBA, es nuestro deber detectarlos, analizarlos y proporcionar comentarios a los desarrolladores.
La realidad es que estos errores son particulares de cada aplicación, por lo que deberá verificarlos uno por uno y no hay una guía que le indique cómo solucionarlos. Teniendo esto en cuenta, hay algunas cosas que puede buscar.
Consejos para investigar y evitar interbloqueos
Busque transacciones de larga duración. Como los bloqueos generalmente se mantienen hasta el final de una transacción, cuanto más larga sea la transacción, más largos serán los bloqueos sobre los recursos. Si es posible, intente dividir las transacciones de larga duración en otras más pequeñas/más rápidas.
A veces, no es posible dividir las transacciones, por lo que el trabajo debe centrarse en tratar de ejecutar esas operaciones en un orden coherente cada vez, de modo que las transacciones formen colas bien definidas y no se bloqueen.
Una solución alternativa que también puede proponer es agregar una lógica de reintento en la aplicación (por supuesto, primero intente resolver el problema subyacente) de manera que, si ocurre un interbloqueo, la aplicación volverá a ejecutar los mismos comandos.
Verifique los niveles de aislamiento utilizados, a veces intente cambiarlos. Busque comandos como SELECCIONAR PARA ACTUALIZAR y SELECCIONAR PARA COMPARTIR, ya que generan bloqueos explícitos y evalúe si realmente son necesarios o si puede trabajar con una instantánea anterior de los datos. Una cosa que puede probar si no puede eliminar estos comandos es usar un nivel de aislamiento más bajo, como LECTURA COMPROMETIDA.
Por supuesto, siempre agregue índices bien elegidos a sus tablas. Luego, sus consultas deben escanear menos registros de índice y, en consecuencia, establecer menos bloqueos.
En un nivel superior, como DBA, puede tomar algunas precauciones para minimizar el bloqueo en general. Para nombrar un ejemplo, en este caso para PostgreSQL, puede evitar agregar un valor predeterminado en el mismo comando que agregará una columna. La modificación de una tabla obtendrá un bloqueo realmente agresivo, y establecer un valor predeterminado para ella actualizará las filas existentes que tienen valores nulos, lo que hace que esta operación lleve mucho tiempo. Entonces, si divide esta operación en varios comandos, agregando la columna, agregando el valor predeterminado, actualizando los valores nulos, minimizará el impacto del bloqueo.
Por supuesto, hay muchos consejos como este que los administradores de bases de datos obtienen con la práctica (crear índices al mismo tiempo, crear el índice pk por separado antes de agregar el pk, etc.), pero lo importante es aprender y comprender esta "manera de pensando" y siempre para minimizar el impacto de bloqueo de las operaciones que estamos haciendo.
Resumen
Con suerte, este blog le ha brindado información útil sobre los puntos muertos de la base de datos y cómo superarlos. Dado que no existe una forma segura de evitar los interbloqueos, saber cómo funcionan puede ayudarlo a detectarlos antes de que dañen las instancias de su base de datos. Las soluciones de software como ClusterControl pueden ayudarlo a garantizar que sus bases de datos siempre se mantengan en forma. ClusterControl ya ha ayudado a cientos de empresas, ¿será la suya la próxima? Descargue su versión de prueba gratuita de ClusterControl hoy mismo para ver si se ajusta a sus necesidades de base de datos.