Udostępnij za pośrednictwem


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 je modyfikują, optymistyczna współbieżność nie stosuje blokad, ale powoduje, że modyfikacja danych kończy się niepowodzeniem przy zapisywaniu, jeśli dane uległy zmianie od czasu 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 EF Core optymistyczna współbieżność jest wdrażana poprzez skonfigurowanie danej właściwości jako tokena współbieżności. Token współbieżności jest ładowany i śledzony podczas zapytania jednostki — podobnie jak każda inna właściwość. Następnie, gdy operacja aktualizacji lub usuwania jest wykonywana podczas SaveChanges(), wartość tokenu współbieżności w bazie danych jest porównywana z oryginalną wartością odczytaną przez 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 = await context.People.SingleAsync(b => b.FirstName == "John");
person.FirstName = "Paul";
await context.SaveChangesAsync();
  1. W pierwszym kroku z bazy danych ładowana jest Osoba, co obejmuje token współbieżności, który jest teraz śledzony jak zwykle przez Entity Framework wraz z resztą właściwości.
  2. Obiekt osoby jest następnie modyfikowany w jakiś sposób — zmieniamy właściwość FirstName.
  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ć, że oprócz użycia PersonId w klauzuli WHERE, EF Core dodał również warunek dla Version; zmodyfikuje to wiersz tylko wtedy, gdy kolumna Version nie zmieniła się od momentu jej zainicjowania w zapytaniu.

W normalnym ("optymistycznym") przypadku nie występuje żadna współbieżna aktualizacja, a UPDATE zakończy się pomyślnie, modyfikując wiersz. Baza danych zgłasza do EF Core, że jedna wiersz została zmieniona przez UPDATE, zgodnie z oczekiwaniami. Jeśli jednak wystąpiła współbieżna aktualizacja, polecenie UPDATE nie znajduje pasujących wierszy i raportuje, że zero zostało zmienionych. W związku z tym program EF Core SaveChanges() zgłasza element DbUpdateConcurrencyException, który aplikacja musi przechwytywać i obsługiwać odpowiednio. Techniki stosowane w tym celu są szczegółowo opisane poniżej, w sekcji Rozwiązywanie 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 zwykle 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 nim zarządzać 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żesz mieć właściwość zawierającą buforowaną lub nieistotną wartość, a ty 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 = await context.People.SingleAsync(b => b.FirstName == "John");
person.FirstName = "Paul";
person.Version = Guid.NewGuid();
await context.SaveChangesAsync();

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 poinformowanie użytkownika, że aktualizacja nie powiodła się z powodu konfliktu zmian; użytkownik może wtedy załadować nowe dane i spróbować ponownie. Lub jeśli aplikacja wykonuje automatyczną aktualizację, może po prostu powtarzać i spróbować ponownie 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 konfliktu współbieżności to:

  1. Przechwyć DbUpdateConcurrencyException podczas SaveChanges.
  2. Użyj DbUpdateConcurrencyException.Entries, 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. Powtarzaj proces, aż nie wystąpią konflikty.

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

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

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

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

                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 do kontroli 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 poziom izolacji transakcji typu 'powtarzalne odczyty'. 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 wiersz Person w celu jego zaktualizowania, baza danych musi upewnić się, że żadne inne transakcje nie zakłócają tego wiersza do momentu zakończenia transakcji. W zależności od implementacji bazy danych odbywa się to na jeden z dwóch sposobów:

  1. Gdy wiersz jest zapytany, transakcja przyjmuje na nim współdzieloną blokadę. Każda transakcja zewnętrzna próbująca zaktualizować wiersz zostanie zablokowana do momentu zakończenia transakcji. Jest to forma pesymistycznego blokowania, implementowana przez poziom izolacji 'powtarzalny odczyt' w SQL Server.
  2. Zamiast blokować, baza danych umożliwia zewnętrznej transakcji zaktualizowanie wiersza, ale gdy twoja własna transakcja spróbuje wykonać aktualizację, zostanie zgłoszony błąd "serializacji", wskazując, że wystąpił konflikt współbieżności. Jest to forma optymistycznego blokowania – podobna do funkcji tokenu równoczesności EF – i jest implementowana przez poziom izolacji migawkowej SQL Server, a także 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 wywołuje wyjątek i zmusza do ponowienia próby, co również ma wpływ. Dotyczy to poziomu odczytu powtarzalnego w SQL Server, ale nie dla poziomu migawki, który nie blokuje wierszy zapytań.

Co ważniejsze, takie podejście wymaga transakcji, która obejmuje 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.