Jak poprawnie wdrożyć optymistyczne blokowanie w MySQL

13

Jak poprawnie wdrożyć optymistyczne blokowanie w MySQL?

Nasz zespół wydedukował, że musimy zrobić nr 4 poniżej, w przeciwnym razie istnieje ryzyko, że inny wątek może zaktualizować tę samą wersję rekordu, ale chcielibyśmy potwierdzić, że jest to najlepszy sposób.

  1. Utwórz pole wersji w tabeli, w której chcesz użyć blokowania optymistycznego, np. Nazwa kolumny = „wersja”
  2. W zaznaczeniach pamiętaj o dołączeniu kolumny wersji i zanotuj wersję
  3. Po kolejnej aktualizacji rekordu instrukcja aktualizacji powinna wydać „gdzie wersja = X” gdzie X jest wersją otrzymaną w # 2 i ustawić pole wersji podczas tej instrukcji aktualizacji na X + 1
  4. Wykonaj SELECT FOR UPDATErekord, który zamierzamy zaktualizować, aby serializować, kto może wprowadzić zmiany w rekordzie, który próbujemy zaktualizować.

Aby to wyjaśnić, staramy się, aby dwa wątki, które wybierają ten sam rekord w tym samym oknie czasowym, w którym pobierają tę samą wersję rekordu, nie nadpisywały się nawzajem, gdyby próbowały zaktualizować rekord w tym samym czasie. Uważamy, że jeśli nie zrobimy # 4, istnieje szansa, że ​​jeśli oba wątki wejdą w odpowiednie transakcje w tym samym czasie (ale nie wydały jeszcze swoich aktualizacji), to kiedy przejdą do aktualizacji, drugi wątek, który użyje aktualizacji ... gdzie wersja = X będzie działać na starych danych.

Czy słusznie myślimy, że musimy dokonać tego pesymistycznego blokowania podczas aktualizacji, nawet jeśli korzystamy z pól wersji / blokowania optymistycznego?

Najlepsze praktyki
źródło
Jaki jest problem? Zwiększasz numer wersji za pomocą UPDATE, a następnie druga UPDATE nie powiedzie się, ponieważ numer wersji nie jest taki sam, jak w momencie odczytu - co jest tym, czego chcesz.
AndreKR,
Czy jesteś pewien? Nie jest jasne, że o ile nie ustawisz poziomu izolacji transakcji na określone ustawienie, faktycznie zobaczysz aktualizację innych wątków. Jeśli oboje wejdziecie w transakcję w tym samym czasie, drugi wątek bardzo dobrze zobaczy OLD dane, gdy idzie na aktualizację. MySQL nie jest tak niezawodny na arenie ACID, jak powiedzmy Oracle, dlatego poszukuje najlepszych praktyk do wdrożenia optymistycznego blokowania w MySQL, które zapobiegną brudnym odczytom / aktualizacjom.
BestPractices,
Ale wtedy transakcja i tak się nie powiedzie podczas zatwierdzania, prawda?
AndreKR,
Wskazuje się, że należy wybrać opcję aktualizacji, aby poradzić sobie z tą sytuacją: dev.mysql.com/doc/refman/5.0/en/innodb-consistent-read.html
BestPractices
@BestPractices Trzeba albo SELECT ... FOR UPDATE czy zamek optymistyczne wersji wiersza, a nie obu. Zobacz szczegóły w odpowiedzi.
Craig Ringer

Odpowiedzi:

17

Twój programista się myli. Potrzebujesz wersji SELECT ... FOR UPDATE lub wiersza, a nie obu.

Wypróbuj i przekonaj się. Otwarte trzy sesje MySQL (A), (B)a (C)do tej samej bazy danych.

W (C)numerze:

CREATE TABLE test(
    id integer PRIMARY KEY,
    data varchar(255) not null,
    version integer not null
);
INSERT INTO test(id,data,version) VALUES (1,'fred',0);
BEGIN;
LOCK TABLES test WRITE;

W obu (A)i (B)wydać UPDATEże testy oraz zestawy wersji wiersza, zmiana winnertekstu w każdym więc można zobaczyć, który jest sesja, która:

-- In (A):

BEGIN;
UPDATE test SET data = 'winnerA',
            version = version + 1
WHERE id = 1 AND version = 0;

-- in (B):

BEGIN;
UPDATE test SET data = 'winnerB',
            version = version + 1
WHERE id = 1 AND version = 0;

Teraz (C), UNLOCK TABLES;aby zwolnić blokadę.

(A)i (B)będzie walczył o blokadę rzędów. Jeden z nich wygra i zdobędzie zamek. Drugi zablokuje się na zamku. Zwycięzca, który uzyskał blokadę, przystąpi do zmiany wiersza. Zakładając, (A)że wygrał, możesz teraz zobaczyć zmieniony wiersz (nadal niezaangażowany, więc niewidoczny dla innych transakcji) za pomocą SELECT * FROM test WHERE id = 1.

COMMITPowiedzmy, że teraz w sesji zwycięzcy (A).

(B)otrzyma blokadę i rozpocznie aktualizację. Jednak wersja nie jest już zgodna, więc nie zmieni żadnych wierszy, jak wynika z wyniku liczby wierszy. Tylko jeden UPDATEmiał jakikolwiek efekt, a aplikacja kliencka może wyraźnie zobaczyć, które się UPDATEpowiodły, a które nie. Dalsze blokowanie nie jest konieczne.

Zobacz dzienniki sesji na pastebin tutaj . Użyłem mysql --prompt="A> "itp., Aby łatwo odróżnić sesje. Skopiowałem i wkleiłem wynik przepleciony w sekwencji czasowej, więc nie jest to całkowicie surowy wynik i możliwe, że popełniłem błędy podczas kopiowania i wklejania. Sprawdź to sam, aby zobaczyć.


Gdybyś nie dodał pole wersji rząd, wtedy trzeba by SELECT ... FOR UPDATEmóc wiarygodnie zapewnić zamówieniu.

Jeśli się nad tym zastanowisz, a SELECT ... FOR UPDATEjest całkowicie zbędne, jeśli natychmiast robisz to UPDATEbez ponownego wykorzystywania danych z SELECTlub jeśli używasz wersji wiersza. I UPDATEtak zabierze zamek. Jeśli ktoś inny zaktualizuje wiersz między Twoim odczytem a późniejszym zapisem, Twoja wersja nie będzie już zgodna, więc aktualizacja się nie powiedzie. Tak działa optymistyczne blokowanie.

Celem SELECT ... FOR UPDATEjest:

  • Aby zarządzać kolejnością blokowania, aby uniknąć zakleszczenia; i
  • Aby rozszerzyć zakres blokady wiersza, gdy chcesz odczytać dane z wiersza, zmień go w aplikacji i napisz nowy wiersz, który jest oparty na oryginalnym, bez konieczności stosowania SERIALIZABLEizolacji lub wersjonowania wiersza.

Nie musisz używać zarówno optymistycznego blokowania (wersjonowanie wierszy), jak i SELECT ... FOR UPDATE. Użyj jednego lub drugiego.

Craig Ringer
źródło
Dzięki Craig. Miałeś rację - programista się pomylił. Dziękujemy za uruchomienie tego testu.
BestPractices,
Co z serwerem SQL? Czy zawsze aktualizowany jest blokada niezależnie od poziomu izolacji transakcji?
plalx,
@plalx Co mówi dokumentacja? Co się stanie, jeśli przeprowadzisz interaktywny test taki jak ten?
Craig Ringer
@CraigRinger, co się stanie, jeśli B dostanie blokadę przed zatwierdzeniem A, ale po aktualizacji A?
MengT
1
@MengT Nie może, dlatego jest to zamek.
Craig Ringer,
0
UPDATE tbl SET owner = $me,
               id = LAST_INSERT_ID(id)
    WHERE owner = ''
    LIMIT 1;
$id = SELECT LAST_INSERT_ID();
Do some stuff (arbitrarily long time)...;
UPDATE  tbl SET owner = '' WHERE id = $id;

Brak blokad (bez tabeli, bez transakcji), a nawet pożądanych:

  • AKTUALIZACJA jest atomowa
  • LAST_INSERT_ID () jest specyficzny dla sesji, a zatem bezpieczny dla wątków.
Rick James
źródło