Mysql
 sql >> Baza danych >  >> RDS >> Mysql

Zapytanie MySQL Update — czy warunek „gdzie” będzie przestrzegany w przypadku wyścigu i blokowania wierszy? (php, PDO, MySQL, InnoDB)

Podczas wyścigu przestrzegany będzie warunek, ale musisz być ostrożny podczas sprawdzania, kto wygrał wyścig.

Rozważ poniższą demonstrację, jak to działa i dlaczego musisz być ostrożny.

Najpierw skonfiguruj kilka minimalnych tabel.

CREATE TABLE table1 (
`id` TINYINT UNSIGNED NOT NULL PRIMARY KEY,
`locked` TINYINT UNSIGNED NOT NULL,
`updated_by_connection_id` TINYINT UNSIGNED DEFAULT NULL
) ENGINE = InnoDB;

CREATE TABLE table2 (
`id` TINYINT UNSIGNED NOT NULL PRIMARY KEY
) ENGINE = InnoDB;

INSERT INTO table1
(`id`,`locked`)
VALUES
(1,0);

id pełni rolę id w Twojej tabeli updated_by_connection_id działa jak assignedPhone i locked jak reservationCompleted .

Teraz zacznijmy test wyścigowy. Powinieneś mieć otwarte 2 okna wiersza poleceń/terminala, połączone z mysql i korzystające z bazy danych, w której utworzyłeś te tabele.

Połączenie 1

start transaction;

Połączenie 2

start transaction;

Połączenie 1

UPDATE table1
SET locked = 1,
updated_by_connection_id = 1
WHERE id = 1
AND locked = 0;

Połączenie 2

UPDATE table1
SET locked = 1,
updated_by_connection_id = 2
WHERE id = 1
AND locked = 0;

Połączenie 2 teraz czeka

Połączenie 1

SELECT * FROM table1 WHERE id = 1;
commit;

W tym momencie połączenie 2 zostaje zwolnione, aby kontynuować i wyświetla następujące informacje:

Połączenie 2

SELECT * FROM table1 WHERE id = 1;
commit;

Wszystko wygląda dobrze. Widzimy, że tak, klauzula WHERE była przestrzegana w sytuacji wyścigu.

Powodem, dla którego powiedziałem, że musisz być ostrożny, jest to, że w prawdziwej aplikacji rzeczy nie zawsze są takie proste. MOŻESZ mieć inne działania w ramach transakcji, które mogą faktycznie zmienić wyniki.

Zresetujmy bazę danych w następujący sposób:

delete from table1;
INSERT INTO table1
(`id`,`locked`)
VALUES
(1,0);

A teraz rozważmy sytuację, w której SELECT jest wykonywany przed UPDATE.

Połączenie 1

start transaction;

SELECT * FROM table2;

Połączenie 2

start transaction;

SELECT * FROM table2;

Połączenie 1

UPDATE table1
SET locked = 1,
updated_by_connection_id = 1
WHERE id = 1
AND locked = 0;

Połączenie 2

UPDATE table1
SET locked = 1,
updated_by_connection_id = 2
WHERE id = 1
AND locked = 0;

Połączenie 2 teraz czeka

Połączenie 1

SELECT * FROM table1 WHERE id = 1;
SELECT * FROM table1 WHERE id = 1 FOR UPDATE;
commit;

W tym momencie połączenie 2 zostaje zwolnione, aby kontynuować i wyświetla następujące informacje:

Ok, zobaczmy kto wygrał:

Połączenie 2

SELECT * FROM table1 WHERE id = 1;

Czekaj, co? Dlaczego locked 0 i updated_by_connection_id NULL??

To jest bycie ostrożnym, o którym wspomniałem. Winowajcą jest właściwie to, że wybraliśmy na początku. Aby uzyskać poprawny wynik, możemy uruchomić następujące polecenie:

SELECT * FROM table1 WHERE id = 1 FOR UPDATE;
commit;

Używając SELECT ... FOR UPDATE możemy uzyskać właściwy wynik. Może to być bardzo mylące (jak to było dla mnie pierwotnie), ponieważ SELECT i SELECT ... FOR UPDATE dają dwa różne wyniki.

Powodem tego jest domyślny poziom izolacji READ-REPEATABLE . Po dokonaniu pierwszego SELECT, zaraz po start transaction; , tworzona jest migawka. Wszystkie przyszłe odczyty bez aktualizacji zostaną wykonane z tego zrzutu.

Dlatego, jeśli po prostu naiwnie WYBIERZ po wykonaniu aktualizacji, pobierze informacje z oryginalnej migawki, czyli przed wiersz został zaktualizowany. Wykonując SELECT ... FOR UPDATE, wymuszasz uzyskanie prawidłowych informacji.

Jednak znowu, w prawdziwej aplikacji może to stanowić problem. Załóżmy na przykład, że twoje żądanie jest opakowane w transakcję, a po wykonaniu aktualizacji chcesz wyświetlić pewne informacje. Zbieranie i wysyłanie informacji, które mogą być obsługiwane przez oddzielny kod wielokrotnego użytku, którego NIE chcesz zaśmiecać klauzulami FOR UPDATE „na wszelki wypadek”. Prowadziłoby to do frustracji z powodu niepotrzebnego blokowania.

Zamiast tego będziesz chciał wybrać inną ścieżkę. Masz tu wiele opcji.

Jednym z nich jest upewnienie się, że zatwierdzisz transakcję po zakończeniu UPDATE. W większości przypadków jest to prawdopodobnie najlepszy i najprostszy wybór.

Inną opcją jest nie próbować używać SELECT do określenia wyniku. Zamiast tego możesz być w stanie odczytać wiersze, których to dotyczy, i użyć ich (aktualizacja 1 wiersza w porównaniu z aktualizacją 0 wierszy), aby określić, czy AKTUALIZACJA się powiodła.

Inną opcją, z której często korzystam, ponieważ lubię trzymać pojedyncze żądanie (takie jak żądanie HTTP) w pełni opakowane w pojedynczą transakcję, jest upewnienie się, że pierwszą instrukcją wykonywaną w transakcji jest albo UPDATE lub WYBIERZ... DO AKTUALIZACJI . Spowoduje to, że migawka NIE zostanie wykonana, dopóki połączenie nie będzie kontynuowane.

Zresetujmy ponownie naszą testową bazę danych i zobaczmy, jak to działa.

delete from table1;
INSERT INTO table1
(`id`,`locked`)
VALUES
(1,0);

Połączenie 1

start transaction;

SELECT * FROM table1 WHERE id = 1 FOR UPDATE;

Połączenie 2

start transaction;

SELECT * FROM table1 WHERE id = 1 FOR UPDATE;

Połączenie 2 teraz czeka.

Połączenie 1

UPDATE table1
SET locked = 1,
updated_by_connection_id = 1
WHERE id = 1
AND locked = 0;
SELECT * FROM table1 WHERE id = 1;
SELECT * FROM table1 WHERE id = 1 FOR UPDATE;
commit;

Połączenie 2 zostało wydane.

Połączenie 2

+----+--------+--------------------------+
| id | locked | updated_by_connection_id |
+----+--------+--------------------------+
|  1 |      1 |                        1 |
+----+--------+--------------------------+

Tutaj możesz mieć swój kod po stronie serwera, aby sprawdzić wyniki tego SELECT i wiedzieć, że jest dokładny, a nawet nie przechodzić do następnych kroków. Ale dla kompletności skończę jak poprzednio.

UPDATE table1
SET locked = 1,
updated_by_connection_id = 2
WHERE id = 1
AND locked = 0;
SELECT * FROM table1 WHERE id = 1;
SELECT * FROM table1 WHERE id = 1 FOR UPDATE;
commit;

Teraz możesz zobaczyć, że w połączeniu 2 SELECT i SELECT ... FOR UPDATE dają ten sam wynik. Dzieje się tak, ponieważ migawka, z której odczytuje polecenie SELECT, została utworzona dopiero po zatwierdzeniu połączenia 1.

Wracając więc do pierwotnego pytania:Tak, klauzula WHERE jest sprawdzana przez instrukcję UPDATE we wszystkich przypadkach. Musisz jednak uważać na wszelkie SELECT, które możesz wykonywać, aby uniknąć błędnego określenia wyniku tej aktualizacji.

(Tak, inną opcją jest zmiana poziomu izolacji transakcji. Jednak tak naprawdę nie mam z tym doświadczenia i żadnych gotchya, które mogą istnieć, więc nie zamierzam się tym zajmować.)



  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Jak usunąć kolumnę z tabeli w MySQL?

  2. Jak obciąć tabelę za pomocą przygotowanej instrukcji w MySQL?

  3. Klucz obcy MySQL podczas usuwania

  4. Wydajność zapytań w dwóch bazach mysql na tym samym serwerze?

  5. Jak mam zająć się indeksowaniem zapytania z dwoma warunkami zakresu?