En travaillant avec des bases de données, le contrôle de la concurrence est le concept qui garantit que les transactions de base de données sont effectuées simultanément sans violer l'intégrité des données.
Il y a beaucoup de théorie et différentes approches autour de ce concept et comment l'accomplir, mais nous ferons brièvement référence à la façon dont PostgreSQL et MySQL (lors de l'utilisation d'InnoDB) le gèrent, et un problème commun qui peut survenir dans les systèmes hautement concurrents :blocages.
Ces moteurs implémentent le contrôle de la concurrence en utilisant une méthode appelée MVCC (Multiversion Concurrency Control). Dans cette méthode, lorsqu'un élément est mis à jour, les modifications n'écraseront pas les données d'origine, mais à la place, une nouvelle version de l'élément (avec les modifications) sera créée. Ainsi, nous aurons plusieurs versions de l'élément stocké.
L'un des principaux avantages de ce modèle est que les verrous acquis pour interroger (lire) des données n'entrent pas en conflit avec les verrous acquis pour écrire des données, et donc la lecture ne bloque jamais l'écriture, et l'écriture ne bloque jamais la lecture.
Mais, si plusieurs versions du même article sont stockées, quelle version une transaction verra-t-elle ? Pour répondre à cette question, nous devons revoir le concept d'isolation des transactions. Les transactions spécifient un niveau d'isolement, qui définit le degré auquel une transaction doit être isolée des modifications de ressources ou de données effectuées par d'autres transactions. Ce degré est directement lié au verrouillage généré par une transaction, et donc, comme il peut être spécifié au niveau de la transaction, il peut déterminer l'impact qu'une transaction en cours peut avoir sur d'autres transactions en cours.
C'est un sujet très intéressant et long, même si nous n'entrerons pas dans trop de détails dans ce blog. Nous recommandons la documentation officielle de PostgreSQL et MySQL pour une lecture plus approfondie sur ce sujet.
Alors, pourquoi abordons-nous les sujets ci-dessus lorsqu'il s'agit de blocages ? Parce que les commandes sql acquerront automatiquement des verrous pour assurer le comportement MVCC, et le type de verrou acquis dépend de l'isolation de transaction définie.
Il existe plusieurs types de verrous (encore une fois un autre sujet long et intéressant à examiner pour PostgreSQL et MySQL) mais, ce qui est important à leur sujet, c'est comment ils interagissent (plus exactement, comment ils entrent en conflit) les uns avec les autres. Pourquoi donc? Parce que deux transactions ne peuvent pas détenir des verrous de modes en conflit sur le même objet en même temps. Et un détail non mineur, une fois acquis, un verrou est normalement conservé jusqu'à la fin de la transaction.
Voici un exemple PostgreSQL de la manière dont les types de verrouillage entrent en conflit :
Conflit de types de verrouillage PostgreSQLEt pour MySQL :
Conflit de types de verrouillage MySQLX=verrou exclusif IX=verrou exclusif d'intention
S=verrou partagé IS=verrou partagé d'intention
Que se passe-t-il lorsque j'ai deux transactions en cours d'exécution qui souhaitent maintenir des verrous en conflit sur le même objet en même temps ? L'un d'eux obtiendra le cadenas et l'autre devra attendre.
Nous sommes donc maintenant en mesure de vraiment comprendre ce qui se passe pendant une impasse.
Qu'est-ce qu'une impasse alors ? Comme vous pouvez l'imaginer, il existe plusieurs définitions d'un blocage de base de données, mais j'aime ce qui suit pour sa simplicité.
Un blocage de base de données est une situation dans laquelle deux transactions ou plus attendent l'une de l'autre pour abandonner les verrous.
Ainsi, par exemple, la situation suivante nous conduira à une impasse :
Exemple de blocageIci, l'application A obtient un verrou sur la table 1 ligne 1 afin de faire une mise à jour.
En même temps, l'application B obtient un verrou sur la table 2 ligne 2.
Maintenant, l'application A doit obtenir un verrou sur la table 2 ligne 2, afin de continuer l'exécution et de terminer la transaction, mais elle ne peut pas obtenir le verrou car il est détenu par l'application B. L'application A doit attendre que l'application B le libère .
Mais l'application B doit obtenir un verrou sur la table 1 ligne 1, afin de continuer l'exécution et terminer la transaction, mais elle ne peut pas obtenir le verrou car il est détenu par l'application A.
Nous voici donc dans une situation de blocage. L'application A attend la ressource détenue par l'application B pour terminer et l'application B attend la ressource détenue par l'application A. Alors, comment continuer ? Le moteur de base de données détectera le blocage et tuera l'une des transactions, débloquera l'autre et déclenchera une erreur de blocage sur celle qui a été tuée.
Voyons quelques exemples de blocage PostgreSQL et MySQL :
PostgreSQL
Supposons que nous disposions d'une base de données de test contenant des informations sur les pays du monde.
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)
Nous avons deux sessions qui souhaitent apporter des modifications à la base de données.
La première session modifiera le champ de région pour le code NLD et le champ de population pour le code AUS.
La deuxième session modifiera le champ de région pour le code AUS et le champ de population pour le code NLD.
Données du tableau :
code: NLD
region: Western Europe
population: 15864000
code: AUS
region: Australia and New Zealand
population: 18886000
Séance 1 :
world=# BEGIN;
BEGIN
world=# UPDATE country SET region='Europe' WHERE code='NLD';
UPDATE 1
Séance 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 session 2 se bloquera en attendant la fin de la session 1.
Séance 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"
Ici, nous avons notre impasse. Le système a détecté le blocage et a tué la session 1.
Séance 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
Et nous pouvons vérifier que la deuxième session s'est terminée correctement après que le blocage a été détecté et que la session 1 a été tuée (ainsi, le verrou a été libéré).
Pour avoir plus de détails, nous pouvons voir le journal de notre serveur 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
Ici, nous pourrons voir les commandes réelles qui ont été détectées lors d'un blocage.
Téléchargez le livre blanc aujourd'hui PostgreSQL Management &Automation with ClusterControlDécouvrez ce que vous devez savoir pour déployer, surveiller, gérer et faire évoluer PostgreSQLTélécharger le livre blancMySQL
Pour simuler un blocage dans MySQL, nous pouvons procéder comme suit.
Comme avec PostgreSQL, supposons que nous disposions d'une base de données de test contenant des informations sur les acteurs et les films, entre autres.
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)
Nous avons deux processus qui veulent apporter des modifications à la base de données.
Le premier processus modifiera le champ first_name pour l'actor_id 1 et le champ last_name pour l'actor_id 7.
Le deuxième processus modifiera le champ first_name pour l'actor_id 7 et le champ last_name pour l'actor_id 1.
Données du tableau :
actor_id: 1
first_name: PENELOPE
last_name: GUINESS
actor_id: 7
first_name: GRACE
last_name: MOSTEL
Séance 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
Séance 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 session 2 se bloquera en attendant la fin de la session 1.
Séance 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
Ici, nous avons notre impasse. Le système a détecté le blocage et a tué la session 1.
Séance 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
Comme nous pouvons le voir dans l'erreur, comme nous l'avons vu pour PostgreSQL, il y a un blocage entre les deux processus.
Pour plus de détails, nous pouvons utiliser la commande 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)
Sous le titre "LATEST DEADLOCK DEADLOCK", nous pouvons voir les détails de notre blocage.
Pour voir le détail du blocage dans le journal des erreurs mysql, nous devons activer l'option innodb_print_all_deadlocks dans notre base de données.
mysql> set global innodb_print_all_deadlocks=1;
Query OK, 0 rows affected (0.00 sec)
Erreur de journal 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)
Compte tenu de ce que nous avons appris ci-dessus sur la raison pour laquelle les blocages se produisent, vous pouvez voir que nous ne pouvons pas faire grand-chose du côté de la base de données pour les éviter. Quoi qu'il en soit, en tant que DBA, il est de notre devoir de les détecter, de les analyser et de fournir des commentaires aux développeurs.
La réalité est que ces erreurs sont particulières à chaque application, vous devrez donc les vérifier une par une et il n'y a pas de guide pour vous dire comment résoudre ce problème. En gardant cela à l'esprit, vous pouvez rechercher certaines choses.
Conseils pour enquêter et éviter les blocages
Rechercher des transactions de longue durée. Comme les verrous sont généralement maintenus jusqu'à la fin d'une transaction, plus la transaction est longue, plus les verrous sur les ressources sont longs. Si possible, essayez de diviser les transactions de longue durée en transactions plus petites/plus rapides.
Parfois, il n'est pas possible de diviser réellement les transactions, donc le travail doit se concentrer sur l'exécution de ces opérations dans un ordre cohérent à chaque fois, afin que les transactions forment des files d'attente bien définies et ne se bloquent pas.
Une solution de contournement que vous pouvez également proposer consiste à ajouter une logique de nouvelle tentative dans l'application (bien sûr, essayez d'abord de résoudre le problème sous-jacent) de manière à ce que, si un blocage se produit, l'application exécute à nouveau les mêmes commandes.
Vérifiez les niveaux d'isolation utilisés, parfois vous essayez en les changeant. Recherchez des commandes telles que SELECT FOR UPDATE et SELECT FOR SHARE, car elles génèrent des verrous explicites, et évaluez si elles sont vraiment nécessaires ou si vous pouvez travailler avec un ancien instantané des données. Une chose que vous pouvez essayer si vous ne pouvez pas supprimer ces commandes est d'utiliser un niveau d'isolement inférieur tel que READ COMMITTED.
Bien sûr, ajoutez toujours des index bien choisis à vos tables. Ensuite, vos requêtes doivent analyser moins d'enregistrements d'index et, par conséquent, définir moins de verrous.
À un niveau supérieur, en tant que DBA, vous pouvez prendre certaines précautions pour minimiser le verrouillage en général. Pour ne citer qu'un exemple, dans ce cas pour PostgreSQL, vous pouvez éviter d'ajouter une valeur par défaut dans la même commande que vous ajouterez une colonne. La modification d'une table obtiendra un verrou très agressif et la définition d'une valeur par défaut mettra à jour les lignes existantes qui ont des valeurs nulles, ce qui rendra cette opération très longue. Donc, si vous divisez cette opération en plusieurs commandes, en ajoutant la colonne, en ajoutant la valeur par défaut, en mettant à jour les valeurs nulles, vous minimiserez l'impact du verrouillage.
Bien sûr, il y a des tonnes de conseils comme celui-ci que les administrateurs de bases de données obtiennent avec la pratique (créer des index simultanément, créer l'index pk séparément avant d'ajouter le pk, etc.), mais l'important est d'apprendre et de comprendre cette "façon de réflexion" et toujours pour minimiser l'impact de verrouillage des opérations que nous effectuons.
Résumé
J'espère que ce blog vous a fourni des informations utiles sur les blocages de base de données et sur la manière de les surmonter. Puisqu'il n'existe pas de moyen infaillible d'éviter les blocages, savoir comment ils fonctionnent peut vous aider à les détecter avant qu'ils ne nuisent à vos instances de base de données. Des solutions logicielles comme ClusterControl peuvent vous aider à vous assurer que vos bases de données restent toujours en forme. ClusterControl a déjà aidé des centaines d'entreprises - la vôtre sera-t-elle la prochaine ? Téléchargez votre essai gratuit de ClusterControl dès aujourd'hui pour voir s'il répond à vos besoins en matière de base de données.