Zpracování konfliktů souběžnosti

Tip

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á metoda souběžného zpracování

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čte a sleduje, když se entita dotazuje – stejně jako jakákoli jiná vlastnost. Když se pak během operace aktualizace nebo odstranění provede SaveChanges()hodnota tokenu souběžnosti v databázi, porovná se s původní hodnotou přečtenou nástrojem 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 = context.People.Single(b => b.FirstName == "John");
person.FirstName = "Paul";
context.SaveChanges();
  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í – vlastnost změníme FirstName .
  3. Pak dáme EF Core pokyn, aby změny zachovaly. 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, aktualizace nenajde odpovídající řádky a sestavy, které byly ovlivněny nulou. 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 se však při přidávání entit nikdy nevyvolá. zatímco databáze může skutečně vyvolat jedinečné narušení omezení, pokud jsou vloženy řádky se stejným klíčem, výsledkem je vyvolána výjimka specifická pro zprostředkovatele, a ne 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 rowversion , že se při aktualizaci řádku automaticky změní, je velmi užitečné jako token souběžnosti s minimálním úsilím, který chrání celý řádek. 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 rowversion uvedený typ je funkce specifická pro SQL Server. Podrobnosti o nastavení tokenu automatické aktualizace souběžnosti se liší v různých databázích 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í konfigurace vlastnosti GUID je 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 = context.People.Single(b => b.FirstName == "John");
person.FirstName = "Paul";
person.Version = Guid.NewGuid();
context.SaveChanges();

Pokud chcete, aby se vždy přiřadil nová hodnota GUID, můžete to udělat pomocí průsečíkuSaveChanges. 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ý, aby implementovaly optimistickou souběžnost, musí vaše aplikace správně zpracovat případ, kdy dojde ke konfliktu souběžnosti a DbUpdateConcurrencyException vyvolá se. Říká se tomu ř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 ke zpracování konfliktů souběžnosti je:

  1. Zachytávat 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. V umístění, kam zahrnete logiku specifickou // TODO: pro aplikaci, je komentář, který zvolí hodnotu, kterou chcete uložit.

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

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ž použijeme náš základní vzorek z výše uvedeného příkladu, když se dotazujeme na Person aktualizaci, aby ji nějakým způsobem aktualizovala, databáze musí zajistit, aby žádné jiné transakce nepřekážely na 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, která značí, ž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, které se pokoušejí upravit stejný řádek, musí blokovat pro celou transakci. To může mít nepříznivý vliv na souběžný výkon (zachovat krátkou transakci!), přestože ef mechanismus vyvolá výjimku a vynutí 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ší prostředky

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