InnoDB-Zeilensperrung - Implementierung


13

Ich habe mich jetzt umgesehen, die MySQL-Site gelesen und kann immer noch nicht genau sehen, wie es funktioniert.

Ich möchte das Ergebnis zum Schreiben auswählen und sperren, die Änderung schreiben und die Sperre aufheben. audocommit ist aktiviert.

planen

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

Wählen Sie ein Element mit dem Status "Ausstehend" aus und aktualisieren Sie es auf "Funktionierend". Verwenden Sie einen exklusiven Schreibzugriff, um sicherzustellen, dass derselbe Artikel nicht zweimal abgeholt wird.

so;

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

Holen Sie sich die ID aus dem Ergebnis

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

Muss ich irgendetwas tun, um die Sperre aufzuheben, und funktioniert es wie oben?

Antworten:


26

Was Sie wollen, ist SELECT ... FOR UPDATE im Kontext einer Transaktion. Mit SELECT FOR UPDATE werden die ausgewählten Zeilen exklusiv gesperrt, so als würden Sie UPDATE ausführen. Es wird auch implizit in der Isolationsstufe READ COMMITTED ausgeführt, unabhängig davon, auf welche Isolationsstufe die explizite Einstellung festgelegt ist. Beachten Sie jedoch, dass SELECT ... FOR UPDATE für die gleichzeitige Verwendung sehr schlecht ist und nur verwendet werden sollte, wenn dies unbedingt erforderlich ist. Es hat auch die Tendenz, sich in einer Codebasis zu vermehren, wenn Leute ausschneiden und einfügen.

Hier ist eine Beispielsitzung aus der Sakila-Datenbank, die einige der Verhaltensweisen von FOR UPDATE-Abfragen demonstriert.

Stellen Sie zunächst die Transaktionsisolationsstufe auf REPEATABLE READ ein, damit wir uns ganz klar fühlen. Dies ist normalerweise nicht erforderlich, da dies die Standardisolationsstufe für InnoDB ist:

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)    

Aktualisieren Sie in der anderen Sitzung diese Zeile. Linda heiratete und änderte ihren Namen:

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

Zurück in Session 1, weil wir in REPEATABLE READ waren, ist Linda immer noch 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)

Aber jetzt wollen wir exklusiven Zugriff auf diese Zeile, deshalb rufen wir FOR UPDATE für die Zeile auf. Beachten Sie, dass wir jetzt die neueste Version der Zeile zurückerhalten, die in Sitzung2 außerhalb dieser Transaktion aktualisiert wurde. Das ist nicht REPEATABLE READ, das ist READ COMMITTED

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)

Lassen Sie uns die in session1 festgelegte Sperre testen. Beachten Sie, dass session2 die Zeile nicht aktualisieren kann.

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

Aber wir können immer noch davon auswählen

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)

Eine untergeordnete Tabelle mit einer Fremdschlüsselbeziehung kann weiterhin aktualisiert werden

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;

Ein weiterer Nebeneffekt ist, dass Sie die Wahrscheinlichkeit eines Deadlocks erheblich erhöhen.

In Ihrem speziellen Fall möchten Sie wahrscheinlich:

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;

Wenn das Teil "Anderes erledigen" unnötig ist und Sie eigentlich keine Informationen über die umliegende Zeile aufbewahren müssen, ist SELECT FOR UPDATE unnötig und verschwenderisch und Sie können stattdessen einfach ein Update ausführen:

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

Ich hoffe, das ergibt einen Sinn.


3
Vielen Dank. Es scheint mein Problem nicht zu lösen, wenn zwei Threads mit "SELECT id FROM itemsWHERE status= 'pending' LIMIT 1 FOR UPDATE;" und sie sehen beide die gleiche Reihe, dann wird einer den anderen verriegeln. Ich hatte gehofft, dass es irgendwie in der Lage sein würde, die gesperrte Reihe zu umgehen und zum nächsten Gegenstand zu gelangen, der noch aussteht.
Wizzard,

1
Die Natur von Datenbanken ist, dass sie konsistente Daten zurückgeben. Wenn Sie diese Abfrage zweimal ausführen, bevor der Wert aktualisiert wurde, erhalten Sie dasselbe Ergebnis zurück. Es gibt keine mir bekannte SQL-Erweiterung, um den ersten Wert zu ermitteln, der dieser Abfrage entspricht, es sei denn, die Zeile ist gesperrt. Dies klingt verdächtig, als würden Sie eine Warteschlange über einer relationalen Datenbank implementieren. Ist das der fall
Aaron Brown

Aaron; Ja, das ist es, was ich versuche zu tun. Ich habe versucht, so etwas wie einen Schalthebel zu benutzen - aber das war eine Pleite. Sie haben noch etwas vor?
Wizzard

Ich denke, Sie sollten dies lesen: engineyard.com/blog/2011/… - für Nachrichtenwarteschlangen gibt es viele, abhängig von der Sprache Ihres Kunden. ActiveMQ, Resque (Ruby + Redis), ZeroMQ, RabbitMQ usw.
Aaron Brown

Wie kann ich sicherstellen, dass Sitzung 2 beim Lesen blockiert wird, bis die Aktualisierung in Sitzung 1 festgeschrieben ist?
CMCDragonkai

2

Wenn Sie die InnoDB-Speicher-Engine verwenden, wird das Sperren auf Zeilenebene verwendet. In Verbindung mit der Multi-Versionierung führt dies zu einer guten Parallelität der Abfragen, da eine bestimmte Tabelle gleichzeitig von verschiedenen Clients gelesen und geändert werden kann. Die Parallelitätseigenschaften auf Zeilenebene lauten wie folgt:

Verschiedene Clients können dieselben Zeilen gleichzeitig lesen.

Verschiedene Clients können verschiedene Zeilen gleichzeitig ändern.

Verschiedene Clients können nicht gleichzeitig dieselbe Zeile ändern. Wenn eine Transaktion eine Zeile ändert, können andere Transaktionen dieselbe Zeile nicht ändern, bis die erste Transaktion abgeschlossen ist. Andere Transaktionen können die geänderte Zeile auch nicht lesen, es sei denn, sie verwenden die Isolationsstufe READ UNCOMMITTED. Das heißt, sie sehen die ursprüngliche, unveränderte Zeile.

Grundsätzlich müssen Sie keine explizite Sperre angeben. InnoDB behandelt dies selbst, obwohl Sie in einigen Situationen nachfolgend möglicherweise explizite Sperrdetails zur expliziten Sperre angeben müssen:

In der folgenden Liste werden die verfügbaren Sperrtypen und ihre Auswirkungen beschrieben:

LESEN

Schließt einen Tisch zum Lesen. Eine READ-Sperre sperrt eine Tabelle für Leseabfragen wie SELECT, die Daten aus der Tabelle abrufen. Es sind keine Schreibvorgänge wie INSERT, DELETE oder UPDATE zulässig, die die Tabelle ändern, auch nicht von dem Client, der die Sperre hält. Wenn eine Tabelle zum Lesen gesperrt ist, können andere Clients gleichzeitig aus der Tabelle lesen, aber kein Client kann darauf schreiben. Ein Client, der in eine Tabelle schreiben möchte, die schreibgeschützt ist, muss warten, bis alle Clients, die gerade davon lesen, ihre Sperren beendet und freigegeben haben.

SCHREIBEN

Schließt einen Tisch zum Schreiben. Eine WRITE-Sperre ist eine exklusive Sperre. Es kann nur erworben werden, wenn eine Tabelle nicht verwendet wird. Einmal erworben, kann nur der Client, der die Schreibsperre besitzt, aus der Tabelle lesen oder in die Tabelle schreiben. Andere Clients können weder davon lesen noch darauf schreiben. Kein anderer Client kann die Tabelle zum Lesen oder Schreiben sperren.

LOKAL LESEN

Sperrt eine Tabelle zum Lesen, lässt jedoch gleichzeitige Einfügungen zu. Eine gleichzeitige Einfügung ist eine Ausnahme vom Prinzip "Leser blockieren Schreiber". Dies gilt nur für MyISAM-Tabellen. Wenn eine MyISAM-Tabelle keine Lücken in der Mitte aufweist, die sich aus gelöschten oder aktualisierten Datensätzen ergeben, werden Einfügungen immer am Ende der Tabelle vorgenommen. In diesem Fall kann ein Client, der aus einer Tabelle liest, sie mit einer READ LOCAL-Sperre sperren, damit andere Clients in die Tabelle einfügen können, während der Client, der die Lesesperre hält, aus ihr liest. Wenn eine MyISAM-Tabelle Lücken aufweist, können Sie diese mit OPTIMIZE TABLE entfernen, um die Tabelle zu defragmentieren.


Danke für die Antwort. Da ich diese Tabelle und 100 Clients habe, die nach ausstehenden Elementen suchen, gab es viele Kollisionen - 2-3 Clients erhielten dieselbe ausstehende Zeile. Tischsperre ist zu langsam.
Wizzard

0

Eine andere Alternative wäre das Hinzufügen einer Spalte, in der die Zeit der letzten erfolgreichen Sperre gespeichert ist, und alles andere, das die Zeile sperren wollte, müsste warten, bis sie entweder gelöscht wurde oder 5 Minuten (oder was auch immer) vergangen sind.

Etwas wie...

Schema

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

lastlock ist ein int, da es den Unix-Zeitstempel speichert, mit dem es einfacher (und vielleicht schneller) zu vergleichen ist.

// Entschuldigen Sie die Semantik, ich habe nicht überprüft, ob sie tatsächlich ausgeführt wird, aber sie sollten nah genug sein, wenn sie nicht ausgeführt werden.

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

Überprüfen Sie dann, wie viele Zeilen aktualisiert wurden, da Zeilen nicht von zwei Prozessen gleichzeitig aktualisiert werden können. Wenn Sie die Zeile aktualisiert haben, haben Sie die Sperre erhalten. Angenommen, Sie verwenden PHP, dann würden Sie mysql_affected_rows () verwenden. Wenn der Rückgabewert 1 ist, haben Sie ihn erfolgreich gesperrt.

Dann können Sie entweder die letzte Sperre auf 0 aktualisieren, nachdem Sie das getan haben, was Sie tun müssen, oder Sie können faul sein und 5 Minuten warten, bis der nächste Sperrversuch trotzdem erfolgreich ist.

BEARBEITEN: Möglicherweise müssen Sie ein wenig arbeiten, um zu überprüfen, ob es um Sommerzeitänderungen wie erwartet funktioniert, da die Uhren eine Stunde zurückgehen und den Scheck möglicherweise ungültig machen. Sie müssten sicherstellen, dass die Unix-Zeitstempel in UTC sind - was auch immer der Fall sein mag.


-1

Alternativ können Sie die Datensatzfelder fragmentieren, um paralleles Schreiben zu ermöglichen und die Zeilensperre zu umgehen (fragmentierter JSON-Pair-Stil). Wenn also ein Feld eines zusammengesetzten Lesedatensatzes eine Ganzzahl / ein reeller Wert wäre, könnten Sie Fragment 1-8 dieses Feldes haben (8 Schreibdatensätze / Zeilen in Kraft). Summieren Sie dann die Fragmente nach jedem Schreiben in einer separaten Lesesuche. Dies ermöglicht bis zu 8 gleichzeitige Benutzer gleichzeitig.

Da Sie nur mit jedem Fragment arbeiten, das eine Teilsumme erstellt, gibt es keine Kollision und echte parallele Aktualisierungen (dh Sie schreiben jedes Fragment und nicht den gesamten einheitlichen Lesedatensatz). Dies funktioniert natürlich nur bei numerischen Feldern. Etwas, das auf mathematischen Modifikationen beruht, um ein Ergebnis zu speichern.

Somit können mehrere Schreibfragmente pro Einheitslesefeld pro Einheitslesedatensatz aufgezeichnet werden. Diese numerischen Fragmente eignen sich auch für ECC, Verschlüsselung und Übertragung / Speicherung auf Blockebene. Je mehr Schreibfragmente vorhanden sind, desto höher sind die parallelen / gleichzeitigen Schreibzugriffsgeschwindigkeiten für gesättigte Daten.

MMORPG leiden massiv unter diesem Problem, wenn eine große Anzahl von Spielern anfängt, sich gegenseitig mit den Fähigkeiten des Wirkungsbereichs zu schlagen. Diese mehreren Spieler müssen alle genau zur gleichen Zeit parallel schreiben / aktualisieren, wodurch ein Schreibreihen-Sperrsturm für vereinheitlichte Spielerdatensätze erzeugt wird.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.