Verrouillage de ligne InnoDB - comment mettre en œuvre

13

J'ai regardé autour de moi maintenant, lisant le site mysql et je ne vois toujours pas exactement comment cela fonctionne.

Je veux sélectionner et verrouiller le résultat pour l'écriture, écrire la modification et libérer le verrou. audocommit est activé.

schème

id (int)
name (varchar50)
status (enum 'pending', 'working', 'complete')
created (datetime)
updated (datetime) 

Sélectionnez un élément dont l'état est En attente et mettez-le à jour pour qu'il fonctionne. Utilisez une écriture exclusive pour vous assurer que le même article n'est pas ramassé deux fois.

donc;

"SELECT id FROM `items` WHERE `status`='pending' LIMIT 1 FOR WRITE"

obtenir l'id du résultat

"UPDATE `items` SET `status`='working', `updated`=NOW() WHERE `id`=<selected id>

Dois-je faire quelque chose pour libérer le verrou, et cela fonctionne-t-il comme je l'ai fait ci-dessus?

Wizzard
la source

Réponses:

26

Ce que vous voulez, c'est SELECT ... FOR UPDATE dans le contexte d'une transaction. SELECT FOR UPDATE met un verrou exclusif sur les lignes sélectionnées, comme si vous exécutiez UPDATE. Il s'exécute également implicitement dans le niveau d'isolement READ COMMITTED, quel que soit le niveau d'isolation explicitement défini. Sachez simplement que SELECT ... FOR UPDATE est très mauvais pour la concurrence et ne doit être utilisé qu'en cas de nécessité absolue. Il a également tendance à se multiplier dans une base de code au fur et à mesure que les gens copient et collent.

Voici un exemple de session de la base de données Sakila qui montre certains des comportements des requêtes FOR UPDATE.

Tout d'abord, pour que tout soit clair, définissez le niveau d'isolement des transactions sur REPEATABLE READ. Ceci n'est normalement pas nécessaire, car il s'agit du niveau d'isolement par défaut pour InnoDB:

session1> SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
session1> BEGIN;
session1> SELECT first_name, last_name FROM customer WHERE customer_id = 3;
+------------+-----------+
| first_name | last_name |
+------------+-----------+
| LINDA      | WILLIAMS  |
+------------+-----------+
1 row in set (0.00 sec)    

Dans l'autre session, mettez à jour cette ligne. Linda s'est mariée et a changé de nom:

session2> UPDATE customer SET last_name = 'BROWN' WHERE customer_id = 3;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

De retour en session1, parce que nous étions en REPEATABLE READ, Linda est toujours LINDA WILLIAMS:

session1> SELECT first_name, last_name FROM customer WHERE customer_id = 3;
+------------+-----------+
| first_name | last_name |
+------------+-----------+
| LINDA      | WILLIAMS  |
+------------+-----------+
1 row in set (0.00 sec)

Mais maintenant, nous voulons un accès exclusif à cette ligne, nous appelons donc FOR UPDATE sur la ligne. Notez que nous récupérons maintenant la version la plus récente de la ligne, qui a été mise à jour dans la session2 en dehors de cette transaction. Ce n'est pas RÉPÉTABLE LIRE, c'est LIRE ENGAGÉ

session1> SELECT first_name, last_name FROM customer WHERE customer_id = 3 FOR UPDATE;
+------------+-----------+
| first_name | last_name |
+------------+-----------+
| LINDA      | BROWN     |
+------------+-----------+
1 row in set (0.00 sec)

Testons le verrou défini dans session1. Notez que session2 ne peut pas mettre à jour la ligne.

session2> UPDATE customer SET last_name = 'SMITH' WHERE customer_id = 3;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

Mais nous pouvons toujours en choisir

session2> SELECT c.customer_id, c.first_name, c.last_name, a.address_id, a.address FROM customer c JOIN address a USING (address_id) WHERE c.customer_id = 3;
+-------------+------------+-----------+------------+-------------------+
| customer_id | first_name | last_name | address_id | address           |
+-------------+------------+-----------+------------+-------------------+
|           3 | LINDA      | BROWN     |          7 | 692 Joliet Street |
+-------------+------------+-----------+------------+-------------------+
1 row in set (0.00 sec)

Et nous pouvons toujours mettre à jour une table enfant avec une relation de clé étrangère

session2> UPDATE address SET address = '5 Main Street' WHERE address_id = 7;
Query OK, 1 row affected (0.05 sec)
Rows matched: 1  Changed: 1  Warnings: 0

session1> COMMIT;

Un autre effet secondaire est que vous augmentez considérablement votre probabilité de provoquer un blocage.

Dans votre cas spécifique, vous souhaitez probablement:

BEGIN;
SELECT id FROM `items` WHERE `status`='pending' LIMIT 1 FOR UPDATE;
-- do some other stuff
UPDATE `items` SET `status`='working', `updated`=NOW() WHERE `id`=<selected id>;
COMMIT;

Si l'élément "faire d'autres choses" n'est pas nécessaire et que vous n'avez pas réellement besoin de conserver des informations sur la ligne, alors SELECT FOR UPDATE est inutile et inutile et vous pouvez simplement exécuter une mise à jour:

UPDATE `items` SET `status`='working', `updated`=NOW() WHERE `status`='pending' LIMIT 1;

J'espère que cela a du sens.

Aaron Brown
la source
3
Merci. Cela ne semble pas résoudre mon problème, lorsque deux threads arrivent avec "SELECT id FROM itemsWHERE status= 'en attente' LIMIT 1 FOR UPDATE;" et ils voient tous les deux la même ligne, puis l'un verrouille l'autre. J'espérais en quelque sorte qu'il pourrait contourner la ligne verrouillée et passer à l'élément suivant qui était en attente ..
Wizzard
1
La nature des bases de données est qu'elles renvoient des données cohérentes. Si vous exécutez cette requête deux fois avant la mise à jour de la valeur, vous obtiendrez le même résultat. Il n'y a aucune extension SQL "obtenez-moi la première valeur qui correspond à cette requête, à moins que la ligne ne soit verrouillée". Cela ressemble étrangement à l'implémentation d'une file d'attente au-dessus d'une base de données relationnelle. Est-ce le cas?
Aaron Brown
Aaron; oui c'est ce que j'essaie de faire. J'ai envisagé d'utiliser quelque chose comme Gearman - mais c'était un buste. Vous avez autre chose en tête?
Wizzard
Je pense que vous devriez lire ceci: engineyard.com/blog/2011/… - pour les files d'attente de messages, il y en a beaucoup selon la langue de votre choix. ActiveMQ, Resque (Ruby + Redis), ZeroMQ, RabbitMQ, etc.
Aaron Brown
Comment faire pour que la session 2 se bloque en lecture jusqu'à ce que la mise à jour de la session 1 soit validée?
CMCDragonkai
2

Si vous utilisez le moteur de stockage InnoDB, il utilise le verrouillage au niveau des lignes. En conjonction avec la multi-version, cela se traduit par une bonne simultanéité des requêtes car une table donnée peut être lue et modifiée par différents clients en même temps. Les propriétés de concurrence au niveau des lignes sont les suivantes:

Différents clients peuvent lire simultanément les mêmes lignes.

Différents clients peuvent modifier différentes lignes simultanément.

Différents clients ne peuvent pas modifier la même ligne en même temps. Si une transaction modifie une ligne, les autres transactions ne peuvent pas modifier la même ligne tant que la première transaction n'est pas terminée. Les autres transactions ne peuvent pas non plus lire la ligne modifiée, sauf si elles utilisent le niveau d'isolement READ UNCOMMITTED. Autrement dit, ils verront la ligne d'origine non modifiée.

Fondamentalement, vous n'avez pas à spécifier de verrouillage explicite InnoDB le gère iteslf bien que dans certaines situations, vous devrez peut-être fournir des détails de verrouillage explicites sur le verrouillage explicite ci-dessous:

La liste suivante décrit les types de verrous disponibles et leurs effets:

LIS

Verrouille une table pour la lecture. Un verrou READ verrouille une table pour les requêtes de lecture telles que SELECT qui récupèrent les données de la table. Il n'autorise pas les opérations d'écriture telles que INSERT, DELETE ou UPDATE qui modifient la table, même par le client qui détient le verrou. Lorsqu'une table est verrouillée pour la lecture, d'autres clients peuvent lire à partir de la table en même temps, mais aucun client ne peut y écrire. Un client qui souhaite écrire dans une table verrouillée en lecture doit attendre que tous les clients qui y lisent actuellement aient terminé et libéré leurs verrous.

ÉCRIRE

Verrouille une table pour l'écriture. Une serrure WRITE est une serrure exclusive. Il ne peut être acquis que lorsqu'une table n'est pas utilisée. Une fois acquis, seul le client détenant le verrou d'écriture peut lire ou écrire dans la table. D'autres clients ne peuvent ni y lire ni y écrire. Aucun autre client ne peut verrouiller la table en lecture ou en écriture.

LIRE LOCAL

Verrouille une table pour la lecture, mais autorise les insertions simultanées. Un encart simultané est une exception au principe du "bloc de lecture des écrivains". Il s'applique uniquement aux tables MyISAM. Si une table MyISAM n'a pas de trous au milieu résultant d'enregistrements supprimés ou mis à jour, des insertions ont toujours lieu à la fin de la table. Dans ce cas, un client qui lit à partir d'une table peut la verrouiller avec un verrou READ LOCAL pour permettre à d'autres clients de s'insérer dans la table pendant que le client qui détient le verrou de lecture en lit. Si une table MyISAM a des trous, vous pouvez les supprimer en utilisant OPTIMIZE TABLE pour défragmenter la table.

Mahesh Patil
la source
Merci d'avoir répondu. Comme j'ai ce tableau et 100 clients vérifiant les éléments en attente, je recevais beaucoup de collisions - 2-3 clients obtenant la même ligne en attente. Le verrouillage de la table doit ralentir.
Wizzard
0

Une autre alternative serait d'ajouter une colonne qui stockait l'heure du dernier verrouillage réussi, puis tout ce qui voulait verrouiller la ligne devrait attendre jusqu'à ce qu'elle soit effacée ou que 5 minutes (ou autre) se soient écoulées.

Quelque chose comme...

Schema

id (int)
name (varchar50)
status (enum 'pending', 'working', 'complete')
created (datetime)
updated (datetime)
lastlock (int)

lastlock est un int car il stocke l'horodatage unix comme son plus facile (et peut-être plus rapide) à comparer.

// Excusez la sémantique, je n'ai pas vérifié qu'ils fonctionnent correctement, mais ils devraient être assez proches s'ils ne le font pas.

UPDATE items 
  SET lastlock = UNIX_TIMESTAMP() 
WHERE 
  lastlock = 0
  OR (UNIX_TIMESTAMP() - lastlock) > 360;

Vérifiez ensuite combien de lignes ont été mises à jour, car les lignes ne peuvent pas être mises à jour par deux processus à la fois, si vous avez mis à jour la ligne, vous avez obtenu le verrou. En supposant que vous utilisez PHP, vous utiliseriez mysql_affected_rows (), si le retour de celui-ci était 1, vous l'avez verrouillé avec succès.

Ensuite, vous pouvez soit mettre à jour le dernier verrou à 0 après avoir fait ce que vous devez faire, soit être paresseux et attendre 5 minutes lorsque la prochaine tentative de verrouillage réussira de toute façon.

EDIT: Vous devrez peut-être un peu de travail pour vérifier que cela fonctionne comme prévu autour des changements d'heure d'été car les horloges remontent d'une heure, ce qui rendrait peut-être le chèque nul. Vous devez vous assurer que les horodatages Unix sont en UTC - ce qu'ils peuvent être de toute façon.

Steve Childs
la source
-1

Vous pouvez également fragmenter les champs d'enregistrement pour permettre l'écriture parallèle et contourner le verrouillage de ligne (style de paires json fragmentées). Donc, si un champ d'un enregistrement de lecture composé était un entier / réel, vous pouvez avoir les fragments 1-8 de ce champ (8 enregistrements / lignes d'écriture en vigueur). Additionnez ensuite les fragments round-robin après chaque écriture dans une recherche de lecture distincte. Cela permet jusqu'à 8 utilisateurs simultanés en parallèle.

Comme vous ne travaillez qu'avec chaque fragment créant un total partiel, il n'y a pas de collision et de véritables mises à jour parallèles (c'est-à-dire que vous écrivez verrouillez chaque fragment plutôt que l'ensemble de l'enregistrement de lecture unifié). Cela ne fonctionne évidemment que sur les champs numériques. Quelque chose qui repose sur une modification mathématique pour stocker un résultat.

Ainsi, plusieurs fragments d'écriture par champ de lecture unifié par enregistrement de lecture unifié. Ces fragments numériques se prêtent également à l'ECC, au chiffrement et au transfert / stockage au niveau du bloc. Plus il y a de fragments d'écriture, plus les vitesses d'accès en écriture parallèle / simultanée sur les données saturées sont élevées.

Les MMORPG souffrent massivement de ce problème, quand un grand nombre de joueurs commencent tous à se frapper avec des compétences de zone d'effet. Ces plusieurs joueurs doivent tous écrire / mettre à jour tous les autres joueurs exactement en même temps, en parallèle, créant une tempête de verrouillage de ligne d'écriture sur les enregistrements de joueurs unifiés.

Mick Saunders
la source