Jak uniknąć zakleszczenia mysql podczas próby uzyskania blokady; spróbuj ponownie uruchomić transakcję ”

286

Mam tabelę innoDB, która rejestruje użytkowników online. Jest aktualizowany przy każdym odświeżeniu strony przez użytkownika, aby śledzić, na których stronach się znajduje i kiedy miał datę ostatniego dostępu do witryny. Następnie mam crona, który działa co 15 minut, aby usunąć stare rekordy.

Podczas próby uzyskania blokady znaleziono „Zakleszczenie”; spróbuj ponownie uruchomić transakcję przez ostatnią noc przez około 5 minut i wydaje się, że dzieje się tak podczas uruchamiania INSERTów w tej tabeli. Czy ktoś może zasugerować, jak uniknąć tego błędu?

=== EDYCJA ===

Oto uruchomione zapytania:

Pierwsza wizyta na stronie:

INSERT INTO onlineusers SET
ip = 123.456.789.123,
datetime = now(),
userid = 321,
page = '/thispage',
area = 'thisarea',
type = 3

Odśwież stronę na każdej stronie:

UPDATE onlineusers SET
ips = 123.456.789.123,
datetime = now(),
userid = 321,
page = '/thispage',
area = 'thisarea',
type = 3
WHERE id = 888

Cron co 15 minut:

DELETE FROM onlineusers WHERE datetime <= now() - INTERVAL 900 SECOND

Następnie rejestruje niektóre statystyki (np. Członkowie online, odwiedzający online).

David
źródło
Czy możesz podać więcej szczegółów na temat struktury tabeli? Czy są jakieś indeksy klastrowe lub nieklastrowane?
Anders Abel,
13
dev.mysql.com/doc/refman/5.1/en/innodb-deadlocks.html - Uruchomienie „pokaż status innodb silnika” zapewni użyteczną diagnostykę.
Martin
Synchroniczne zapisywanie bazy danych nie jest dobrą praktyką, gdy użytkownicy wchodzą na stronę. właściwym sposobem na to jest trzymanie go w pamięci, takiej jak memcache lub szybka kolejka i zapisywanie w db za pomocą crona.
Nir

Odpowiedzi:

292

Prostą sztuczką, która może pomóc w większości zakleszczeń, jest sortowanie operacji w określonej kolejności.

Otrzymujesz impas, gdy dwie transakcje próbują zablokować dwie blokady na przeciwnych zleceniach, tj .:

  • połączenie 1: blokuje klucz (1), blokuje klucz (2);
  • połączenie 2: blokuje klucz (2), blokuje klucz (1);

Jeśli oba działają w tym samym czasie, połączenie 1 zablokuje klucz (1), połączenie 2 zablokuje klucz (2) i każde połączenie będzie czekać, aż drugie zwolni klucz -> zakleszczenie.

Teraz, jeśli zmieniłeś swoje zapytania tak, że połączenia zablokowałyby klucze w tej samej kolejności, tj .:

  • połączenie 1: blokuje klucz (1), blokuje klucz (2);
  • połączenie 2: blokuje klucz ( 1 ), blokuje klucz ( 2 );

impas nie będzie możliwy.

Oto co sugeruję:

  1. Upewnij się, że nie masz innych zapytań blokujących dostęp do więcej niż jednego klucza na raz, z wyjątkiem instrukcji delete. jeśli to zrobisz (i podejrzewam, że tak), zamów ich GDZIE w (k1, k2, .. kn) w porządku rosnącym.

  2. Napraw instrukcję usuwania, aby działała w porządku rosnącym:

Zmiana

DELETE FROM onlineusers WHERE datetime <= now() - INTERVAL 900 SECOND

Do

DELETE FROM onlineusers WHERE id IN (SELECT id FROM onlineusers
    WHERE datetime <= now() - INTERVAL 900 SECOND order by id) u;

Inną rzeczą, o której należy pamiętać, jest to, że dokumentacja mysql sugeruje, że w przypadku impasu klient powinien ponowić próbę automatycznie. możesz dodać tę logikę do kodu klienta. (Powiedz, 3 próby tego konkretnego błędu przed rezygnacją).

Omry Yadan
źródło
2
Jeśli mam transakcję (autocommit = false), zgłoszony zostanie wyjątek zakleszczenia. Czy wystarczy powtórzyć tę samą instrukcję. ExecuteUpdate (), czy cała transakcja jest teraz aktywowana i powinna zostać cofnięta + uruchom ponownie wszystko, co w niej działało?
Kogo
5
jeśli masz włączone transakcje, to wszystko albo nic. jeśli miałeś wyjątek jakiegokolwiek rodzaju, jest gwarantowane, że cała transakcja nie przyniosła żadnego efektu. w takim przypadku chciałbyś zrestartować całość.
Omry Yadan
4
Usuwanie oparte na zaznaczeniu na ogromnym stole jest bardzo wolniejsze niż zwykłe usuwanie
Thermech
3
Dziękuję bardzo, stary. Wskazówka dotycząca instrukcji sortowania rozwiązała moje problemy z martwym zamkiem.
Miere
4
@OmryYadan Z tego co wiem, w MySQL nie można wybrać podkwerendy z tej samej tabeli, w której dokonuje się AKTUALIZACJI. dev.mysql.com/doc/refman/5.7/en/update.html
artaxerxe
72

Zakleszczenie występuje, gdy dwie transakcje oczekują na siebie w celu uzyskania blokady. Przykład:

  • Tx 1: zamknij A, a następnie B
  • TX 2: zamek B, a następnie A

Istnieje wiele pytań i odpowiedzi na temat impasu. Za każdym razem, gdy wstawiasz / aktualizujesz / lub usuwasz wiersz, tworzona jest blokada. Aby uniknąć zakleszczenia, musisz upewnić się, że równoczesne transakcje nie aktualizują wiersza w kolejności, która może spowodować zakleszczenie. Ogólnie rzecz biorąc, spróbuj uzyskać blokadę zawsze w tej samej kolejności, nawet w różnych transakcjach (np. Zawsze najpierw tabela A, a następnie tabela B).

Innym powodem impasu w bazie danych może być brak indeksów . Po wstawieniu / aktualizacji / usunięciu wiersza baza danych musi sprawdzić ograniczenia relacyjne, czyli upewnić się, że relacje są spójne. Aby to zrobić, baza danych musi sprawdzić klucze obce w powiązanych tabelach. Może to spowodować uzyskanie innej blokady niż zmodyfikowany wiersz. Pamiętaj, aby zawsze mieć indeks na kluczach obcych (i oczywiście kluczach podstawowych), w przeciwnym razie może to spowodować blokadę tabeli zamiast blokady wiersza . Jeśli nastąpi blokada stołu, rywalizacja o blokadę jest wyższa, a prawdopodobieństwo impasu rośnie.

ewernli
źródło
3
Być może moim problemem jest to, że użytkownik odświeżył stronę i w ten sposób uruchomiła AKTUALIZACJĘ rekordu w tym samym czasie, gdy cron próbuje uruchomić DELETE na rekordzie. Dostaję jednak błąd na WSTAWKI, aby cron nie usuwał właśnie utworzonych rekordów. Jak więc może dojść do impasu w rekordzie, który ma zostać jeszcze wstawiony?
David
Czy możesz podać nieco więcej informacji o tabelach i tym, co dokładnie robią transakcje?
ewernli
Nie rozumiem, jak może dojść do impasu, jeśli na transakcję przypada tylko jedno oświadczenie. Żadnych innych operacji na innych stołach? Brak specjalnych kluczy obcych lub unikalnych ograniczeń? Brak ograniczeń usuwania kaskadowego?
ewernli
nie, nic innego specjalnego ... Przypuszczam, że jest to związane z charakterem użycia stołu. wiersz jest wstawiany / aktualizowany przy każdym odświeżeniu strony przez użytkownika. Jednocześnie ponad 1000 odwiedzających jest aktywnych.
David
12

Jest prawdopodobne, że instrukcja delete wpłynie na dużą część wszystkich wierszy w tabeli. Ostatecznie może to prowadzić do uzyskania blokady tabeli podczas usuwania. Trzymanie się blokady (w tym przypadku blokad wierszy lub stron) i uzyskiwanie większej liczby blokad zawsze stanowi ryzyko zakleszczenia. Jednak nie potrafię wyjaśnić, dlaczego instrukcja insert prowadzi do eskalacji blokady - może mieć to związek z dzieleniem / dodawaniem stron, ale ktoś, kto lepiej zna MySQL, będzie musiał tam wypełnić.

Na początek warto spróbować od razu uzyskać blokadę tabeli dla instrukcji delete. Zobacz BLOKADA TABEL i Problemy z blokowaniem tabel .

Anders Abel
źródło
6

Możesz spróbować deleteuruchomić to zadanie, najpierw wstawiając klucz każdego wiersza, który ma zostać usunięty, do tabeli tymczasowej, takiej jak ten pseudokod

create temporary table deletetemp (userid int);

insert into deletetemp (userid)
  select userid from onlineusers where datetime <= now - interval 900 second;

delete from onlineusers where userid in (select userid from deletetemp);

Rozbicie go w ten sposób jest mniej wydajne, ale pozwala uniknąć konieczności trzymania blokady zakresu klawiszy podczas delete.

Zmodyfikuj również swoje selectzapytania, aby dodać whereklauzulę z wyłączeniem wierszy starszych niż 900 sekund. Pozwala to uniknąć zależności od zadania cron i pozwala na rzadsze uruchamianie go.

Teoria o zakleszczeniach: Nie mam zbyt dużego tła w MySQL, ale oto jest ... deleteZamierza utrzymać blokadę zakresu kluczy dla datetime, aby zapobiec wheredodawaniu wierszy pasujących do klauzuli w środku transakcji , a gdy znajdzie wiersze do usunięcia, spróbuje uzyskać blokadę na każdej modyfikowanej stronie. insertZamierza nabyć blokadę na stronie jest włożeniem, a następnie próbować pozyskać blokady. Zwykle insertbędzie cierpliwie czekał na otwarcie tej blokady klucza, ale nastąpi impas, jeśli deletespróbuje zablokować tę samą stronę, której insertużywa, ponieważ deletepotrzebuje tej strony i inserttej blokady. Nie wydaje się to jednak odpowiednie dla wstawek, deleteiinsert używają zakresów czasu, które się nie pokrywają, więc może dzieje się coś innego.

http://dev.mysql.com/doc/refman/5.1/en/innodb-next-key-locking.html

Brian Sandlin
źródło
4

Jeśli ktoś nadal ma problem z tym problemem:

Napotkałem podobny problem, gdy 2 żądania uderzały jednocześnie o serwer. Nie było takiej sytuacji jak poniżej:

T1:
    BEGIN TRANSACTION
    INSERT TABLE A
    INSERT TABLE B
    END TRANSACTION

T2:
    BEGIN TRANSACTION
    INSERT TABLE B
    INSERT TABLE A
    END TRANSACTION

Byłem więc zdziwiony, dlaczego tak się dzieje.

Potem odkryłem, że między 2 tabelami istniała relacja rodzicielska z powodu klucza obcego. Kiedy wstawiałem rekord do tabeli podrzędnej, transakcja uzyskiwała blokadę w wierszu tabeli nadrzędnej. Zaraz potem próbowałem zaktualizować wiersz nadrzędny, który wyzwalał podniesienie blokady do WYŁĄCZNIE jednego. Ponieważ druga jednoczesna transakcja już posiadała blokadę SHARED, powodowała impas.

Zobacz: https://blog.tekenlight.com/2019/02/21/database-deadlock-mysql.html

czat
źródło
Również w moim przypadku wygląda na to, że problemem była relacja klucza obcego. Dzięki 1
Chris Prince
3

W przypadku programistów Java korzystających z Spring uniknąłem tego problemu, stosując aspekt AOP, który automatycznie ponawia próby transakcji, które napotykają przejściowe zakleszczenia.

Aby uzyskać więcej informacji, zobacz @RetryTransaction Javadoc.

Archie
źródło
0

Mam metodę, której elementy wewnętrzne są opakowane w transakcję MySqlTransaction.

Problem impasu pojawił się dla mnie, gdy uruchomiłem tę samą metodę równolegle z samym sobą.

Nie wystąpił problem z uruchomieniem pojedynczej instancji metody.

Kiedy usunąłem MySqlTransaction, byłem w stanie uruchomić metodę równolegle ze sobą bez żadnych problemów.

Po prostu dzieląc się swoim doświadczeniem, nic nie zalecam.

CINCHAPPS
źródło
0

cronjest niebezpieczny. Jeśli jedno wystąpienie crona nie zakończy się przed kolejnym terminem, prawdopodobnie będą ze sobą walczyć.

Lepiej byłoby mieć ciągle działające zadanie, które usuwałoby niektóre wiersze, spało niektóre, a następnie powtarzało.

Jest również INDEX(datetime)bardzo ważny dla uniknięcia zakleszczeń.

Ale jeśli test datetime obejmuje więcej niż, powiedzmy, 20% tabeli, DELETEskanuje tabelę. Mniejsze fragmenty usuwane częściej to obejście.

Innym powodem wyboru mniejszych kawałków jest zablokowanie mniejszej liczby rzędów.

Dolna linia:

  • INDEX(datetime)
  • Ciągle uruchomione zadanie - usuń, przespaj minutę, powtórz.
  • Aby upewnić się, że powyższe zadanie nie umarło, przygotuj zadanie crona, którego jedynym celem jest ponowne uruchomienie go po awarii.

Inne techniki usuwania: http://mysql.rjweb.org/doc.php/deletebig

Rick James
źródło