Blokowanie wierszy InnoDB - jak zaimplementować

13

Rozglądam się teraz, czytając stronę mysql i nadal nie widzę dokładnie, jak to działa.

Chcę wybrać i zablokować wiersz do zapisu, zapisać zmianę i zwolnić blokadę. audocommit jest włączony.

schemat

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

Wybierz element ze stanem oczekującym i zaktualizuj go do działania. Użyj ekskluzywnego zapisu, aby upewnić się, że ten sam przedmiot nie zostanie odebrany dwukrotnie.

więc;

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

pobierz identyfikator z wyniku

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

Czy muszę coś zrobić, aby zwolnić blokadę i czy działa tak, jak wcześniej?

Wizzard
źródło

Odpowiedzi:

26

To, czego chcesz, to WYBIERZ ... DO AKTUALIZACJI w kontekście transakcji. WYBIERZ NA AKTUALIZACJĘ blokuje wyłączne zaznaczenie wybranych wierszy, tak jakbyś wykonywał aktualizację. Działa również domyślnie na poziomie izolacji PRZECZYTAJ ZOBOWIĄZANIE, niezależnie od tego, na jaki poziom izolacji jest jawnie ustawiony. Pamiętaj tylko, że WYBIERZ ... DO AKTUALIZACJI jest bardzo zły dla współbieżności i powinien być używany tylko wtedy, gdy jest to absolutnie konieczne. Ma również tendencję do namnażania się w bazie kodu, gdy ludzie wycinają i wklejają.

Oto przykładowa sesja z bazy danych Sakila, która demonstruje niektóre zachowania zapytań FOR UPDATE.

Po pierwsze, abyśmy byli krystalicznie czysti, ustaw poziom izolacji transakcji na POWTARZALNE CZYTANIE. Jest to zwykle niepotrzebne, ponieważ jest to domyślny poziom izolacji dla 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)    

W drugiej sesji zaktualizuj ten wiersz. Linda wyszła za mąż i zmieniła imię:

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

Z powrotem w sesji 1, ponieważ byliśmy w REPEATABLE READ, Linda nadal jest 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)

Ale teraz chcemy wyłącznego dostępu do tego wiersza, dlatego w wierszu wywołujemy FOR UPDATE. Zauważ, że otrzymujemy teraz najnowszą wersję wiersza, która została zaktualizowana w sesji 2 poza tą transakcją. To NIE POWTARZANE CZYTANIE, to CZYTANIE ZOBOWIĄZANE

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)

Przetestujmy blokadę ustawioną w session1. Pamiętaj, że session2 nie może zaktualizować wiersza.

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

Ale nadal możemy z niego wybierać

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)

Nadal możemy aktualizować tabelę podrzędną za pomocą relacji klucza obcego

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;

Kolejnym efektem ubocznym jest znaczne zwiększenie prawdopodobieństwa spowodowania impasu.

W twoim konkretnym przypadku prawdopodobnie chcesz:

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;

Jeśli fragment „zrób coś innego” jest niepotrzebny i nie musisz tak naprawdę przechowywać informacji o wierszu, wybierz WYBIERZ AKTUALIZACJĘ jest niepotrzebny i marnotrawny, a zamiast tego możesz po prostu uruchomić aktualizację:

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

Mam nadzieję, że to ma jakiś sens.

Aaron Brown
źródło
3
Dzięki. Wydaje się, że to nie rozwiązuje mojego problemu, gdy przychodzą dwa wątki z „SELECT id FROM itemsWHERE status=„ w toku ”LIMIT 1 DO AKTUALIZACJI;” i oboje widzą ten sam rząd, a następnie jeden zablokuje drugi. Miałem nadzieję, że jakoś uda się ominąć zamknięty rząd i przejść do następnego, który był w toku.
Wizzard
1
Charakter baz danych polega na tym, że zwracają one spójne dane. Jeśli wykonasz to zapytanie dwukrotnie przed zaktualizowaniem wartości, otrzymasz ten sam wynik z powrotem. Nie ma „pobierz mi pierwszą wartość pasującą do tego zapytania, chyba że wiersz jest zablokowany” rozszerzenie SQL, o którym wiem. Brzmi to podejrzanie, jakbyś implementował kolejkę na relacyjnej bazie danych. Czy tak jest w przypadku?
Aaron Brown
Aaron; tak właśnie staram się zrobić. Patrzyłem na użycie czegoś takiego jak gearman - ale to było popiersie. Masz na myśli coś jeszcze?
Wizzard
Myślę, że powinieneś przeczytać to: engineyard.com/blog/2011/… - w przypadku kolejek wiadomości jest ich wiele, w zależności od wybranego języka klienta. ActiveMQ, Resque (Ruby + Redis), ZeroMQ, RabbitMQ itp.
Aaron Brown
Jak to zrobić, aby sesja 2 blokowała się podczas odczytu do momentu zatwierdzenia aktualizacji w sesji 1?
CMCDragonkai
2

Jeśli używasz silnika pamięci InnoDB, używa on blokowania na poziomie wiersza. W połączeniu z wersją wielu wersji powoduje to dobrą współbieżność zapytań, ponieważ dana tabela może być odczytywana i modyfikowana przez różnych klientów jednocześnie. Właściwości współbieżności na poziomie wiersza są następujące:

Różni klienci mogą jednocześnie czytać te same wiersze.

Różni klienci mogą modyfikować różne wiersze jednocześnie.

Różni klienci nie mogą modyfikować tego samego wiersza w tym samym czasie. Jeśli jedna transakcja modyfikuje wiersz, inne transakcje nie mogą modyfikować tego samego wiersza, dopóki pierwsza transakcja nie zostanie zakończona. Inne transakcje również nie mogą odczytać zmodyfikowanego wiersza, chyba że używają poziomu izolacji ODCZYTAJ NIEZGODNOŚĆ. Oznacza to, że zobaczą oryginalny niezmodyfikowany wiersz.

Zasadniczo nie musisz określać jawnego blokowania InnoDB obsługuje to samo, chociaż w niektórych sytuacjach może być konieczne podanie jawnych szczegółów blokowania jawnego blokowania podano poniżej:

Poniższa lista opisuje dostępne typy blokad i ich efekty:

CZYTAĆ

Blokuje stolik do czytania. Blokada READ blokuje tabelę dla zapytań odczytu, takich jak SELECT, które pobierają dane z tabeli. Nie zezwala na operacje zapisu, takie jak INSERT, DELETE lub UPDATE, które modyfikują tabelę, nawet przez klienta posiadającego blokadę. Gdy tabela jest zablokowana do odczytu, inni klienci mogą czytać z tabeli w tym samym czasie, ale żaden klient nie może do niej pisać. Klient, który chce pisać do tabeli z blokadą odczytu, musi poczekać, aż wszyscy klienci czytający z niej zakończą i zwolnią blokady.

PISAĆ

Blokuje stół do pisania. Zamek WRITE to zamek wyłączny. Można go zdobyć tylko wtedy, gdy nie jest używany stół. Po uzyskaniu tylko klient posiadający blokadę zapisu może czytać lub zapisywać w tabeli. Inni klienci nie mogą ani czytać, ani pisać. Żaden inny klient nie może zablokować tabeli ani czytać, ani pisać.

PRZECZYTAJ LOKALNIE

Blokuje tabelę do odczytu, ale umożliwia jednoczesne wstawianie. Jednoczesna wstawka stanowi wyjątek od zasady „czytników bloków zapisujących”. Dotyczy tylko tabel MyISAM. Jeśli w tabeli MyISAM nie ma żadnych otworów pośrodku wynikających z usuniętych lub zaktualizowanych rekordów, wstawianie zawsze odbywa się na końcu tabeli. W takim przypadku klient, który czyta z tabeli, może zablokować ją blokadą CZYTAJ LOKALNIE, aby umożliwić innym klientom wstawianie do tabeli, podczas gdy klient trzymający blokadę odczytu czyta z niej. Jeśli w tabeli MyISAM są dziury, możesz je usunąć za pomocą OPTYMALIZACJI TABELI w celu defragmentacji tabeli.

Mahesh Patil
źródło
Dziękuję za odpowiedź. Ponieważ mam tę tabelę i 100 klientów sprawdzających oczekujące elementy, miałem dużo kolizji - 2-3 klientów otrzymywało ten sam oczekujący wiersz. Blokada stołu jest za wolna.
Wizzard
0

Inną alternatywą byłoby dodanie kolumny, która przechowywała czas ostatniej udanej blokady, a następnie wszystko, co chciało zablokować wiersz, musiałoby poczekać, aż zostanie wyczyszczone lub upłynie 5 minut (lub cokolwiek innego).

Coś jak...

Schema

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

lastlock jest int, ponieważ przechowuje uniksowy znacznik czasu jako łatwiejszy (a może i szybszy) do porównania.

// Przepraszam za semantykę, nie sprawdziłem, czy uruchamiają się ostro, ale jeśli nie, powinny być wystarczająco blisko.

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

Następnie sprawdź, ile wierszy zostało zaktualizowanych, ponieważ wiersze nie mogą być aktualizowane przez dwa procesy jednocześnie, jeśli zaktualizowałeś wiersz, dostałeś blokadę. Zakładając, że używasz PHP, użyłbyś mysql_affected_rows (), jeśli zwrot z tego wynosił 1, pomyślnie go zablokowałeś.

Następnie możesz albo zaktualizować ostatnią blokadę do zera po zrobieniu tego, co musisz zrobić, albo być leniwym i czekać 5 minut, aż kolejna próba blokady się powiedzie.

EDYCJA: Może być konieczne trochę pracy, aby sprawdzić, czy działa zgodnie z oczekiwaniami wokół zmian czasu letniego, ponieważ zegary cofną się o godzinę, być może unieważniając czek. Musisz upewnić się, że uniksowe znaczniki czasu były w UTC - które i tak mogą być.

Steve Childs
źródło
-1

Alternatywnie, możesz pofragmentować pola rekordu, aby umożliwić równoległe zapisywanie i ominąć blokowanie wiersza (styl podzielonych par json). Więc jeśli jedno pole w złożonym odczytanym rekordzie było liczbą całkowitą / rzeczywistą, możesz mieć fragment 1-8 tego pola (8 zapisów rekordów / wierszy). Następnie zsumuj fragmenty po każdym zapisie do osobnego odnośnika odczytu. Umożliwia to równolegle do 8 jednoczesnych użytkowników.

Ponieważ pracujesz tylko z każdym fragmentem tworzącym częściową sumę, nie ma kolizji i prawdziwych równoległych aktualizacji (tzn. Piszesz blokować każdy fragment, a nie cały zunifikowany rekord odczytu). Działa to oczywiście tylko na polach numerycznych. Coś, co polega na modyfikacji matematycznej do przechowywania wyniku.

Zatem wiele fragmentów zapisu na zunifikowane pole odczytu na zunifikowany rekord odczytu. Te fragmenty liczbowe nadają się również do ECC, szyfrowania i transferu / przechowywania na poziomie bloków. Im więcej fragmentów zapisu, tym większe prędkości dostępu równoległego / współbieżnego zapisu danych nasyconych.

MMORPG cierpi z powodu tego problemu, gdy duża liczba graczy zaczyna się nawzajem uderzać umiejętnościami Obszar działania. Wszyscy ci gracze muszą pisać / aktualizować każdego innego gracza w tym samym czasie, równolegle, tworząc burzę blokującą wiersz zapisu w rekordach zunifikowanych graczy.

Mick Saunders
źródło