Sdílet prostřednictvím


Zpracování konfliktů souběžnosti

Návod

Ukázku pro tento článek najdete na GitHubu.

Ve většině scénářů se databáze používají souběžně několika instancemi aplikace, přičemž každá provádí změny dat nezávisle na sobě. Když se stejná data změní současně, může dojít k nekonzistence a poškození dat, například když dva klienti upravují různé sloupce ve stejném řádku, které jsou nějakým způsobem související. Tato stránka popisuje mechanismy pro zajištění toho, aby vaše data zůstala konzistentní vzhledem k těmto souběžným změnám.

Optimistická souběžnost

EF Core implementuje optimistickou souběžnost, což předpokládá, že konflikty souběžnosti jsou relativně vzácné. Na rozdíl od pesimistických přístupů – které zamknou data předem a teprve potom je upravíte – optimistická souběžnost nemá žádné zámky, ale zajistí, aby úpravy dat selhaly při uložení, pokud se data od dotazování změnila. Tato chyba souběžnosti je hlášena aplikaci, která se s ní odpovídajícím způsobem zabývá, a to opakováním celé operace s novými daty.

V EF Core se optimistická souběžnost implementuje konfigurací vlastnosti jako token souběžnosti. Token souběžnosti se načítá a sleduje při dotazování na entitu – stejně jako jakákoli jiná vlastnost. Pak, když se během provádění aktualizace nebo odstranění prostřednictvím SaveChanges() porovná hodnota tokenu souběžnosti v databázi s původní hodnotou přečtenou EF Core.

Abychom pochopili, jak to funguje, předpokládejme, že jsme na SQL Serveru, a definujte typický typ entity Person se speciální Version vlastností:

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

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

Na SQL Serveru se tím nakonfiguruje token souběžnosti, který se automaticky změní v databázi při každé změně řádku (další podrobnosti jsou k dispozici níže). S touto konfigurací se podíváme, co se stane s jednoduchou operací aktualizace:

var person = await context.People.SingleAsync(b => b.FirstName == "John");
person.FirstName = "Paul";
await context.SaveChangesAsync();
  1. V prvním kroku se Osoba načte z databáze; to zahrnuje token souběžnosti, který je nyní sledován jako obvykle EF spolu se zbývajícími vlastnostmi.
  2. Instance Person se pak nějakým způsobem upraví – změní se vlastnost FirstName.
  3. Poté instruujeme EF Core, aby uložil změnu. Vzhledem k tomu, že je nakonfigurovaný token souběžnosti, ef Core odešle do databáze následující SQL:
UPDATE [People] SET [FirstName] = @p0
WHERE [PersonId] = @p1 AND [Version] = @p2;

Všimněte si, že kromě PersonId klauzule WHERE přidala EF Core také podmínku Version . Tím se upraví jenom řádek, pokud Version se sloupec od okamžiku, kdy jsme na něj dotazovali, nezměnil.

V normálním ("optimistickém") případě nedojde k žádné souběžné aktualizaci a aktualizace se úspěšně dokončí a upraví řádek; databáze hlásí EF Core, že aktualizace ovlivnila jeden řádek podle očekávání. Pokud však došlo k souběžné aktualizaci, příkaz UPDATE nenajde žádné odpovídající řádky a hlásí, že nebyly ovlivněny žádné řádky. V důsledku toho EF Core SaveChanges() vyvolá DbUpdateConcurrencyException, který aplikace musí zachytit a zpracovat odpovídajícím způsobem. Techniky, jak to udělat, jsou podrobně popsány v části Řešení konfliktů souběžnosti.

I když výše uvedené příklady probíraly aktualizace existujících entit. Ef také vyvolá DbUpdateConcurrencyException při pokusu o odstranění řádku, který byl současně změněn. Tato výjimka však obvykle není vyvolána při přidávání entit; zatímco databáze může skutečně vyvolat porušení jedinečného omezení, pokud jsou vloženy řádky se stejným klíčem, to má za následek vyvolání výjimky specifické pro poskytovatele, a nikoli DbUpdateConcurrencyException.

Nativní tokeny souběžnosti generované databází

Ve výše uvedeném kódu jsme použili [Timestamp] atribut k mapování vlastnosti na sloupec SQL Serveru rowversion . Vzhledem k tomu, že se rowversion při aktualizaci řádku automaticky změní, je velmi užitečný jako konkurenční token pro minimální úsilí, který zajišťuje ochranu celého řádku. Konfigurace sloupce SQL Serveru rowversion jako token souběžnosti se provádí takto:

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

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

Výše uvedený typ rowversion je specifická funkce SQL Serveru. Podrobnosti o konfiguraci automaticky aktualizovatelného tokenu souběžnosti se liší mezi databázemi a některé databáze je vůbec nepodporují (např. SQLite). Přesné podrobnosti najdete v dokumentaci k vašemu poskytovateli.

Tokeny souběžnosti spravované aplikací

Místo toho, aby databáze spravovaly token souběžnosti automaticky, můžete ho spravovat v kódu aplikace. To umožňuje používat optimistickou souběžnost u databází , jako je SQLite, kde neexistuje žádný nativní typ automatické aktualizace. I na SQL Serveru ale token souběžnosti spravovaný aplikací může poskytovat jemně odstupňované řízení přesně toho, které změny ve sloupcích způsobí opětovné generování tokenu. Můžete mít například vlastnost obsahující určitou hodnotu uloženou v mezipaměti nebo nedůležitou hodnotu a nechcete, aby změna této vlastnosti aktivovala konflikt souběžnosti.

Následující konfiguruje vlastnost GUID jako token souběžnosti:

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

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

Vzhledem k tomu, že tato vlastnost není generovaná databáze, musíte ji přiřadit v aplikaci při každém zachování změn:

var person = await context.People.SingleAsync(b => b.FirstName == "John");
person.FirstName = "Paul";
person.Version = Guid.NewGuid();
await context.SaveChangesAsync();

Pokud chcete, aby se vždy přiřadila nová hodnota GUID, můžete to udělat pomocí zachytávačeSaveChanges. Jednou z výhod ruční správy tokenu souběžnosti je, že můžete přesně řídit, kdy se znovu vygeneruje, abyste se vyhnuli zbytečným konfliktům souběžnosti.

Řešení konfliktů souběžnosti

Bez ohledu na to, jak je token souběžnosti nastaven, pokud má být implementována optimistická souběžnost, vaše aplikace musí správně zpracovat situaci, kdy dojde ke konfliktu souběžnosti a DbUpdateConcurrencyException je vyvolána. Tomu se říká řešení konfliktu souběžnosti.

Jednou z možností je jednoduše informovat uživatele, že aktualizace selhala kvůli konfliktních změnám; uživatel pak může načíst nová data a zkusit to znovu. Nebo pokud vaše aplikace provádí automatizovanou aktualizaci, může to jednoduše opakovat a zkusit to okamžitě po opětovném dotazování dat.

Složitější způsob řešení konfliktů souběžnosti spočívá ve sloučení čekajících změn s novými hodnotami v databázi. Přesné podrobnosti o tom, které hodnoty se sloučí, závisí na aplikaci a proces může být směrován uživatelským rozhraním, kde se zobrazí obě sady hodnot.

K dispozici jsou tři sady hodnot, které vám pomůžou vyřešit konflikt souběžnosti:

  • Aktuální hodnoty jsou hodnoty, které se aplikace pokoušela zapsat do databáze.
  • Původní hodnoty jsou hodnoty, které byly původně načteny z databáze před provedením jakýchkoli úprav.
  • Hodnoty databáze jsou hodnoty, které jsou aktuálně uloženy v databázi.

Obecný přístup k řešení konfliktu konkurenčních operací je:

  1. Zachytit DbUpdateConcurrencyException během SaveChanges.
  2. Slouží DbUpdateConcurrencyException.Entries k přípravě nové sady změn pro ovlivněné entity.
  3. Aktualizujte původní hodnoty tokenu souběžnosti tak, aby odrážely aktuální hodnoty v databázi.
  4. Opakujte proces, dokud nedojde ke konfliktům.

V následujícím příkladu Person.FirstName a Person.LastName jsou nastaveny jako tokeny souběžnosti. Na místě, kde je uveden komentář, zahrňte logiku specifickou pro aplikaci k výběru hodnoty, kterou chcete uložit.

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);
            }
        }
    }
}

Použití úrovní izolace pro řízení souběžnosti

Optimistická souběžnost prostřednictvím tokenů souběžnosti není jediným způsobem, jak zajistit, aby data zůstala konzistentní vzhledem ke souběžným změnám.

Jedním z mechanismů, který zajistí konzistenci, je úroveň izolace transakcí s opakovatelným čtením . Ve většině databází tato úroveň zaručuje, že transakce vidí data v databázi, jako byla při spuštění transakce, aniž by byla ovlivněna žádnou následnou souběžnou aktivitou. Když vezmeme základní vzorek z výše uvedeného příkladu a dotazujeme se na Person, abychom jej mohli nějakým způsobem aktualizovat, databáze musí zajistit, že žádné jiné transakce nezasáhnou tento řádek databáze, dokud se transakce nedokončí. V závislosti na implementaci databáze k tomu dochází jedním ze dvou způsobů:

  1. Když je řádek dotazován, vaše transakce převezme sdílený zámek na něj. Všechny externí transakce, které se pokoušejí aktualizovat řádek, budou blokované, dokud se transakce nedokoní. Jedná se o formu pesimistického uzamčení a implementuje se na úrovni izolace "opakovatelného čtení" SQL Serveru.
  2. Místo uzamčení databáze umožňuje externí transakci aktualizovat řádek, ale když se vaše vlastní transakce pokusí provést aktualizaci, vyvolá se chyba serializace, což znamená, že došlo ke konfliktu souběžnosti. Jedná se o formu optimistického zamykání – ne na rozdíl od funkce tokenu souběžnosti EF – a implementuje se na úrovni izolace snímků SQL Serveru a také na úrovni izolace opakovatelného čtení PostgreSQL.

Všimněte si, že úroveň izolace "serializovatelná" poskytuje stejné záruky jako opakovatelné čtení (a přidává další), takže funguje stejným způsobem s ohledem na výše uvedené.

Použití vyšší úrovně izolace ke správě konfliktů souběžnosti je jednodušší, nevyžaduje tokeny souběžnosti a poskytuje další výhody; Například opakovatelné čtení zaručuje, že vaše transakce vždy vidí stejná data napříč dotazy uvnitř transakce, aby nedocházelo k nekonzistence. Tento přístup ale má své nevýhody.

Zaprvé, pokud implementace databáze používá k implementaci úrovně izolace uzamčení, pak ostatní transakce pokoušející se upravit stejný řádek musí být blokovány po celou dobu trvání transakce. To může mít nepříznivý vliv na souběžný výkon (udržujte transakce krátké!), přestože mechanismus EF vyvolá výjimku a přinutí vás k opakování, což má také dopad. To platí pro opakovatelnou úroveň čtení SQL Serveru, ale ne na úroveň snímku, která nezamkne dotazované řádky.

Důležitější je, že tento přístup vyžaduje transakci, která zahrnuje všechny operace. Pokud například zadáte dotaz Person , aby se zobrazily jeho podrobnosti uživateli, a pak počkejte, až uživatel provede změny, pak transakce musí zůstat naživu po potenciálně dlouhou dobu, která by se měla ve většině případů vyhnout. V důsledku toho je tento mechanismus obvykle vhodný, když se okamžitě spustí všechny obsažené operace a transakce nezávisí na externích vstupech, které můžou prodloužit dobu trvání.

Další materiály

Viz Detekce konfliktů v EF Core pro ukázku ASP.NET Core s detekcí konfliktů.