Obsługa konfliktów współbieżności

Napiwek

Przykład z tego artykułu można zobaczyć w witrynie GitHub.

W większości scenariuszy bazy danych są używane współbieżnie przez wiele wystąpień aplikacji, z których każda wykonuje modyfikacje danych niezależnie od siebie. Gdy te same dane zostaną zmodyfikowane w tym samym czasie, mogą wystąpić niespójności i uszkodzenie danych, np. gdy dwaj klienci modyfikują różne kolumny w tym samym wierszu, które są powiązane w jakiś sposób. Na tej stronie omówiono mechanizmy zapewniania, że dane pozostają spójne w obliczu takich współbieżnych zmian.

Optymistyczna współbieżność

Program EF Core implementuje optymistyczną współbieżność, która zakłada, że konflikty współbieżności są stosunkowo rzadkie. W przeciwieństwie do pesymistycznych podejść — które blokują dane z góry, a dopiero potem modyfikują je — optymistyczna współbieżność nie ma blokad, ale organizuje modyfikację danych w celu niepowodzenia zapisywania, jeśli dane uległy zmianie od czasu wykonania zapytania. Ten błąd współbieżności jest zgłaszany do aplikacji, która zajmuje się nim odpowiednio, prawdopodobnie ponawiając próbę wykonania całej operacji na nowych danych.

W programie EF Core optymistyczna współbieżność jest implementowana przez skonfigurowanie właściwości jako tokenu współbieżności. Token współbieżności jest ładowany i śledzony po wysłaniu zapytania do jednostki — podobnie jak w przypadku każdej innej właściwości. Następnie po wykonaniu operacji aktualizacji lub usuwania podczas SaveChanges()programu wartość tokenu współbieżności w bazie danych jest porównywana z oryginalną wartością odczytaną przez program EF Core.

Aby zrozumieć, jak to działa, załóżmy, że jesteśmy w programie SQL Server i zdefiniujmy typowy typ jednostki Person z właściwością specjalną Version :

public class Person
{
    public int PersonId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }

    [Timestamp]
    public byte[] Version { get; set; }
}

W programie SQL Server powoduje to skonfigurowanie tokenu współbieżności, który automatycznie zmienia się w bazie danych za każdym razem, gdy wiersz jest zmieniany (więcej szczegółów można znaleźć poniżej). Po utworzeniu tej konfiguracji sprawdźmy, co się stanie z prostą operacją aktualizacji:

var person = context.People.Single(b => b.FirstName == "John");
person.FirstName = "Paul";
context.SaveChanges();
  1. W pierwszym kroku jest ładowana osoba z bazy danych; Obejmuje to token współbieżności, który jest teraz śledzony jak zwykle przez program EF wraz z resztą właściwości.
  2. Wystąpienie osoby jest następnie modyfikowane w jakiś sposób — zmieniamy FirstName właściwość.
  3. Następnie instruujemy program EF Core, aby utrwał modyfikację. Ponieważ skonfigurowano token współbieżności, program EF Core wysyła następujący kod SQL do bazy danych:
UPDATE [People] SET [FirstName] = @p0
WHERE [PersonId] = @p1 AND [Version] = @p2;

Należy pamiętać PersonId , że oprócz klauzuli WHERE program EF Core dodał również warunek Version . Spowoduje to zmodyfikowanie wiersza tylko wtedy, gdy Version kolumna nie uległa zmianie od momentu, w którym zostało ono zapytane.

W normalnym przypadku ("optymistyczna") nie występuje żadna współbieżna aktualizacja, a aktualizacja zakończy się pomyślnie, modyfikując wiersz; baza danych zgłasza programOWI EF Core, którego dotyczył jeden wiersz, zgodnie z oczekiwaniami. Jeśli jednak wystąpiła współbieżna aktualizacja, aktualizacja nie może odnaleźć pasujących wierszy i raportów, których dotyczy zero. W związku z tym program EF Core SaveChanges() zgłasza element DbUpdateConcurrencyException, który aplikacja musi przechwytywać i obsługiwać odpowiednio. Poniżej przedstawiono techniki rozwiązywania konfliktów współbieżności.

W powyższych przykładach omówiono aktualizacje istniejących jednostek. Program EF zgłasza DbUpdateConcurrencyException również podczas próby usunięcia wiersza, który został jednocześnie zmodyfikowany. Jednak ten wyjątek nigdy nie jest zgłaszany podczas dodawania jednostek; baza danych może rzeczywiście zgłosić unikatowe naruszenie ograniczeń, jeśli wiersze z tym samym kluczem są wstawione, powoduje to zgłoszenie wyjątku specyficznego dla dostawcy, a nie DbUpdateConcurrencyException.

Natywne tokeny współbieżności generowane przez bazę danych

W powyższym kodzie użyliśmy atrybutu [Timestamp] do mapowania właściwości na kolumnę programu SQL Server rowversion . Ponieważ rowversion automatycznie zmienia się po zaktualizowaniu wiersza, jest to bardzo przydatne jako token współbieżności minimalnego nakładu pracy, który chroni cały wiersz. Konfigurowanie kolumny programu SQL Server rowversion jako tokenu współbieżności odbywa się w następujący sposób:

public class Person
{
    public int PersonId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }

    [Timestamp]
    public byte[] Version { get; set; }
}

Powyższy rowversion typ jest funkcją specyficzną dla programu SQL Server. Szczegółowe informacje dotyczące konfigurowania automatycznie aktualizowanego tokenu współbieżności różnią się między bazami danych, a niektóre bazy danych w ogóle ich nie obsługują (np. SQLite). Aby uzyskać szczegółowe informacje, zapoznaj się z dokumentacją dostawcy.

Tokeny współbieżności zarządzane przez aplikację

Zamiast automatycznie zarządzać tokenem współbieżności, możesz zarządzać nią w kodzie aplikacji. Umożliwia to korzystanie z optymistycznej współbieżności w bazach danych , takich jak SQLite, gdzie nie istnieje natywny typ automatycznego aktualizowania. Jednak nawet w programie SQL Server token współbieżności zarządzany przez aplikację może zapewnić szczegółową kontrolę nad dokładnie tym, które zmiany kolumn powodują ponowne wygenerowanie tokenu. Na przykład może istnieć właściwość zawierająca niektóre buforowane lub nieistotne wartości i nie chcesz, aby zmiana tej właściwości wyzwalała konflikt współbieżności.

Poniżej przedstawiono konfigurację właściwości IDENTYFIKATORa GUID jako tokenu współbieżności:

public class Person
{
    public int PersonId { get; set; }
    public string FirstName { get; set; }

    [ConcurrencyCheck]
    public Guid Version { get; set; }
}

Ponieważ ta właściwość nie jest generowana przez bazę danych, należy ją przypisać w aplikacji przy każdym utrwalaniu zmian:

var person = context.People.Single(b => b.FirstName == "John");
person.FirstName = "Paul";
person.Version = Guid.NewGuid();
context.SaveChanges();

Jeśli chcesz, aby nowa wartość identyfikatora GUID zawsze została przypisana, możesz to zrobić za pośrednictwem przechwytnikaSaveChanges. Jedną z zalet ręcznego zarządzania tokenem współbieżności jest to, że można kontrolować dokładnie, kiedy jest generowany ponownie, aby uniknąć niepotrzebnych konfliktów współbieżności.

Rozwiązywanie konfliktów współbieżności

Niezależnie od sposobu konfigurowania tokenu współbieżności, aby zaimplementować optymistyczną współbieżność, aplikacja musi prawidłowo obsługiwać ten przypadek, w którym występuje konflikt współbieżności i DbUpdateConcurrencyException jest zgłaszany. Jest to nazywane rozwiązywaniem konfliktu współbieżności.

Jedną z opcji jest po prostu informowanie użytkownika o tym, że aktualizacja nie powiodła się z powodu zmian powodu konfliktu; użytkownik może następnie załadować nowe dane i spróbować ponownie. Lub jeśli aplikacja wykonuje automatyczną aktualizację, może po prostu zapętlać i ponowić próbę natychmiast po ponownym wykonaniu zapytania o dane.

Bardziej zaawansowanym sposobem rozwiązywania konfliktów współbieżności jest scalenie oczekujących zmian z nowymi wartościami w bazie danych. Szczegółowe informacje o tym, które wartości są scalane, zależą od aplikacji, a proces może być kierowany przez interfejs użytkownika, w którym są wyświetlane oba zestawy wartości.

Dostępne są trzy zestawy wartości, które ułatwiają rozwiązanie konfliktu współbieżności:

  • Bieżące wartości to wartości , które aplikacja próbowała zapisać w bazie danych.
  • Oryginalne wartości to wartości, które zostały pierwotnie pobrane z bazy danych przed dokonaniem jakichkolwiek zmian.
  • Wartości bazy danych to wartości obecnie przechowywane w bazie danych.

Ogólne podejście do obsługi konfliktów współbieżności to:

  1. Przechwyć DbUpdateConcurrencyException podczas wykonywania operacji SaveChanges.
  2. Użyj DbUpdateConcurrencyException.Entries polecenia , aby przygotować nowy zestaw zmian dla jednostek, których dotyczy problem.
  3. Odśwież oryginalne wartości tokenu współbieżności, aby odzwierciedlić bieżące wartości w bazie danych.
  4. Ponów próbę wykonania procesu, dopóki nie wystąpią konflikty.

W poniższym przykładzie Person.FirstName i Person.LastName są konfigurowane jako tokeny współbieżności. Istnieje komentarz w lokalizacji, w której uwzględniasz logikę // TODO: specyficzną dla aplikacji, aby wybrać wartość do zapisania.

using var context = new PersonContext();
// Fetch a person from database and change phone number
var person = context.People.Single(p => p.PersonId == 1);
person.PhoneNumber = "555-555-5555";

// Change the person's name in the database to simulate a concurrency conflict
context.Database.ExecuteSqlRaw(
    "UPDATE dbo.People SET FirstName = 'Jane' WHERE PersonId = 1");

var saved = false;
while (!saved)
{
    try
    {
        // Attempt to save changes to the database
        context.SaveChanges();
        saved = true;
    }
    catch (DbUpdateConcurrencyException ex)
    {
        foreach (var entry in ex.Entries)
        {
            if (entry.Entity is Person)
            {
                var proposedValues = entry.CurrentValues;
                var databaseValues = entry.GetDatabaseValues();

                foreach (var property in proposedValues.Properties)
                {
                    var proposedValue = proposedValues[property];
                    var databaseValue = databaseValues[property];

                    // TODO: decide which value should be written to database
                    // proposedValues[property] = <value to be saved>;
                }

                // Refresh original values to bypass next concurrency check
                entry.OriginalValues.SetValues(databaseValues);
            }
            else
            {
                throw new NotSupportedException(
                    "Don't know how to handle concurrency conflicts for "
                    + entry.Metadata.Name);
            }
        }
    }
}

Używanie poziomów izolacji dla kontrolki współbieżności

Optymistyczna współbieżność za pośrednictwem tokenów współbieżności nie jest jedynym sposobem zapewnienia, że dane pozostają spójne w obliczu współbieżnych zmian.

Jednym z mechanizmów zapewniających spójność jest powtarzalny poziom izolacji transakcji. W większości baz danych ten poziom gwarantuje, że transakcja będzie widzieć dane w bazie danych, tak jak podczas uruchamiania transakcji, bez wpływu na jakiekolwiek kolejne równoczesne działanie. Biorąc nasz podstawowy przykład z góry, gdy wysyłamy zapytanie o Person element w celu zaktualizowania go w jakiś sposób, baza danych nie musi upewnić się, że żadne inne transakcje nie zakłócają tego wiersza bazy danych do momentu zakończenia transakcji. W zależności od implementacji bazy danych odbywa się to na jeden z dwóch sposobów:

  1. Po wysłaniu zapytania do wiersza transakcja przyjmuje na nim udostępnioną blokadę. Każda transakcja zewnętrzna próbująca zaktualizować wiersz zostanie zablokowana do momentu zakończenia transakcji. Jest to forma pesymistycznego blokowania i jest implementowana przez poziom izolacji "powtarzalny odczyt" programu SQL Server.
  2. Zamiast blokować, baza danych umożliwia zewnętrznej transakcji zaktualizowanie wiersza, ale gdy twoja własna transakcja podejmie próbę aktualizacji, zostanie zgłoszony błąd "serializacji", co wskazuje, że wystąpił konflikt współbieżności. Jest to forma optymistycznego blokowania — nie w przeciwieństwie do funkcji tokenu współbieżności ef — i jest implementowana przez poziom izolacji migawki programu SQL Server, a także przez poziom izolacji powtarzalnych odczytów postgreSQL.

Należy pamiętać, że poziom izolacji "z możliwością serializacji" zapewnia takie same gwarancje jak powtarzalny odczyt (i dodaje dodatkowe), dlatego działa w taki sam sposób w odniesieniu do powyższych.

Użycie wyższego poziomu izolacji do zarządzania konfliktami współbieżności jest prostsze, nie wymaga tokenów współbieżności i zapewnia inne korzyści; Na przykład powtarzalne operacje odczytu gwarantują, że transakcja zawsze widzi te same dane między zapytaniami wewnątrz transakcji, unikając niespójności. Jednak takie podejście ma swoje wady.

Po pierwsze, jeśli implementacja bazy danych używa blokady do zaimplementowania poziomu izolacji, inne transakcje próbujące zmodyfikować ten sam wiersz muszą blokować dla całej transakcji. Może to mieć negatywny wpływ na wydajność współbieżną (zachowaj krótki czas transakcji!), chociaż należy pamiętać, że mechanizm ef zgłasza wyjątek i wymusza ponowienie próby, co również ma wpływ. Dotyczy to powtarzalnego poziomu odczytu programu SQL Server, ale nie do poziomu migawki, który nie blokuje zapytanych wierszy.

Co ważniejsze, takie podejście wymaga transakcji, aby obejmowała wszystkie operacje. Jeśli na przykład wykonasz zapytanie Person , aby wyświetlić jego szczegóły użytkownikowi, a następnie poczekaj, aż użytkownik wprowadzi zmiany, transakcja musi pozostać aktywna przez potencjalnie długi czas, którego należy unikać w większości przypadków. W rezultacie ten mechanizm jest zwykle odpowiedni, gdy wszystkie zawarte operacje wykonywane natychmiast, a transakcja nie zależy od danych wejściowych zewnętrznych, które mogą zwiększyć czas trwania.

Dodatkowe zasoby

Zobacz Wykrywanie konfliktów w programie EF Core , aby zapoznać się z przykładem ASP.NET Core z wykrywaniem konfliktów.