web-dev-qa-db-fra.com

Verrouillage de ligne InnoDB - comment mettre en œuvre

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 quoi que ce soit pour libérer le verrou, et cela fonctionne-t-il comme je l'ai fait ci-dessus?

13
Wizzard

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'isolement défini explicitement. 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 à 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.

26
Aaron Brown

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 besoin de spécifier le 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:

LIRE

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.

2
Mahesh Patil

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 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;

Ensuite, vérifiez 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 remonteraient 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.

0
Steve Childs