Zkratka proOptimistic Concurrency Controloptimistic lokingTakéoptimistická souběžnost, optimistic concurrency control, OCC, verzování záznamůPokročilý

Definice

Optimistické zamykání je způsob řízení souběžných zápisů, při kterém se záznam během čtení a úprav nezamyká a konflikt se odhalí až v okamžiku uložení: zápis proběhne jen tehdy, pokud se data mezitím nezměnila. Kontrola se obvykle dělá přes číslo verze nebo časové razítko a při neshodě aplikace operaci zopakuje.

Kategorie: DatabázeAktualizováno

Sázka na to, že se konflikt nestane

Optimistické zamykání vychází z předpokladu, že dva lidé nebo dva procesy málokdy upravují tentýž záznam ve stejnou chvíli. Místo aby se řádek při čtení uzamkl a ostatní čekali, přečte se volně včetně čísla verze. Teprve UPDATE obsahuje podmínku na tuto verzi. Když databáze ohlásí, že se nezměnil žádný řádek, mezi čtením a zápisem někdo jiný data přepsal a transakce se musí zopakovat nebo skončit chybou.

Verze, časové razítko, nebo hash?

Nejspolehlivější je celočíselný sloupec version, který se při každém zápisu zvýší o jedničku. Časové razítko je čitelnější pro člověka, ale při hrubém rozlišení hodin nebo při posunu času mezi uzly může selhat. Hash celého obsahu záznamu funguje bez zásahu do schématu, jenže selhává, když se hodnota změní tam a zpět. PostgreSQL nabízí systémový sloupec xmin, který lze pro tento účel použít bez vlastního sloupce.

Cena za absenci zámků

Optimistické zamykání nic nestojí, dokud ke kolizi nedojde: čtenáři se nikdy neblokují navzájem a nehrozí uváznutí (deadlock) mezi transakcemi držícími zámky. Účet přijde při vysoké míře sporu. Pokud tisíc požadavků upravuje tentýž řádek se skladovou zásobou, většina z nich zápis prohraje a opakuje práci, takže propustnost prudce klesne a systém spálí procesorový čas na neúspěšných pokusech. Pro takový hotspot je pesimistický zámek (SELECT ... FOR UPDATE) nebo atomický dekrement rychlejší.

Kde se s ním setkáte

Optimistické zamykání je výchozím režimem ORM nástrojů jako Hibernate (anotace @Version) a Entity Frameworku. V HTTP jej implementuje hlavička ETag spolu s If-Match: server odmítne PUT s kódem 412 Precondition Failed, pokud se reprezentace mezitím změnila. Podobně pracují dokumentové databáze v ekosystému NoSQL, například DynamoDB s podmíněnými zápisy nebo CouchDB s revizemi dokumentu.

Vztah k izolaci transakcí

Úroveň izolace Serializable v PostgreSQL používá vnitřně stejnou myšlenku: konflikt neblokuje, ale způsobí selhání transakce s chybou serializace. Aplikace tedy musí umět operaci zopakovat i tehdy, když žádný sloupec s verzí nemá. Opakování by mělo být idempotentní a omezené počtem pokusů, jinak se pod zátěží roztočí smyčka nekonečných opakování napříč vlákny.

Co musí umět uživatelské rozhraní

Optimistické zamykání posouvá řešení konfliktu do aplikace, a tím i do rukou uživatele. Formulář, který po dvaceti minutách editace zahlásí jen „záznam byl změněn“ a zahodí text, je horší než čekání na zámek. Dobré rozhraní ukáže, co se změnilo, nabídne sloučení polí, která se nepřekrývají, a data z formuláře zachová.

Příklady z praxe

  1. Úprava profilu se sloupcem version

    Dva administrátoři otevřou stejný záznam zákazníka s hodnotou version = 7. První uloží změnu telefonu, verze stoupne na 8. Druhý o chvíli později odešle svůj UPDATE s podmínkou version = 7, databáze vrátí nula změněných řádků a aplikace mu ukáže konflikt místo tichého přepsání.

    UPDATE zakaznik
    SET email = 'novy@example.com',
        version = version + 1
    WHERE id = 42 AND version = 7;
    -- vrátí 0 řádků => konflikt, načíst znovu a zopakovat
  2. REST API s ETag a If-Match

    Klient si stáhne dokument a spolu s ním hlavičku ETag s hodnotou "a91f". Při ukládání pošle PUT s hlavičkou If-Match: "a91f". Pokud mezitím uložil někdo jiný, server má novou hodnotu ETag a odpoví 412 Precondition Failed. Klient si stáhne aktuální verzi a nabídne uživateli porovnání.

    PUT /api/dokumenty/17 HTTP/1.1
    If-Match: "a91f"
    Content-Type: application/json
    
    {"nadpis": "Upravený nadpis"}
    
    HTTP/1.1 412 Precondition Failed

Časté omyly

MýtusOptimistické zamykání vlastně nic nezamyká, takže data nechrání.
Ve skutečnostiOptimistické zamykání chrání proti ztracené aktualizaci stejně spolehlivě jako zámek, jen konflikt řeší až při zápisu. Podmínka na verzi se vyhodnocuje atomicky uvnitř databáze, takže dva souběžné zápisy nemohou uspět oba.
MýtusKdyž použiju optimistické zamykání, nemusím řešit opakování transakce.
Ve skutečnostiOptimistické zamykání bez logiky opakování jen přesouvá chybu k uživateli. Aplikace musí konflikt zachytit, znovu načíst aktuální stav a operaci buď automaticky zopakovat, nebo nabídnout sloučení změn.
MýtusOptimistické zamykání je vždy rychlejší než pesimistické.
Ve skutečnostiOptimistické zamykání vyhrává jen při nízké míře sporu. U horkého řádku, o který se pere mnoho zapisujících, převáží náklady na opakované neúspěšné pokusy a pesimistický zámek bývá výrazně efektivnější.

Časté dotazy

Jak poznám, že mám v aplikaci nastavit optimistické nebo pesimistické zamykání?
Rozhodujícím ukazatelem je míra sporu o konkrétní řádek. Optimistické zamykání se hodí tam, kde souběžná editace téhož záznamu je výjimkou: uživatelské profily, objednávky, dokumenty, konfigurace. Pesimistický zámek dává smysl u sdíleného čítače, posledního kusu na skladě nebo účetního zůstatku, kde se o stejná data pere mnoho zapisujících naráz. Praktický postup je začít optimisticky a měřit, jak často zápis selhává na neshodu verze. Pokud podíl konfliktů roste nad jednotky procent, přechod na zámek nebo na atomickou operaci přímo v databázi obvykle pomůže víc než ladění opakování.
Kolikrát se má transakce po konfliktu automaticky zopakovat?
Automatické opakování by mělo mít pevný strop, typicky tři až pět pokusů, a mezi pokusy exponenciálně rostoucí prodlevu s náhodným rozptylem. Bez stropu se pod zátěží roztočí lavina opakování, která zhoršuje přesně tu situaci, kterou má řešit. Automaticky lze opakovat jen operace, které nemají vedlejší efekty mimo databázi, nebo které jsou idempotentní: odeslání e-mailu ani platbu nelze slepě zopakovat. Konflikt způsobený lidskou editací se opakovat nemá vůbec, tam patří zobrazení rozdílu a rozhodnutí uživatele.
Funguje optimistické zamykání i napříč více tabulkami?
Optimistické zamykání se přirozeně váže k jednomu záznamu, ale lze ho rozšířit na agregát. Běžný postup je držet sloupec s verzí na kořenové entitě, například na hlavičce objednávky, a zvyšovat ho i při změně podřízených položek. Zápis pak kontroluje jedinou verzi a celý agregát se chová jako jedna jednotka konzistence. Podmínkou je, že všechny změny proběhnou v jedné databázové transakci. Bez toho hrozí, že část zápisu projde a část selže na neshodě verze.
Je hlavička ETag skutečně optimistické zamykání, nebo jde jen o cache?
Hlavička ETag plní obě role podle toho, s jakou podmíněnou hlavičkou se kombinuje. Ve spojení s If-None-Match slouží k ověření platnosti mezipaměti a šetří přenos dat. Ve spojení s If-Match u metod PUT, PATCH a DELETE jde o plnohodnotné optimistické zamykání: server zápis provede jen tehdy, když se reprezentace od odeslané hodnoty neliší, jinak vrátí stavový kód 412 Precondition Failed. Rozdíl je tedy v použití, nikoli v samotné hlavičce.

Zdroje

  1. PostgreSQL Documentation: Concurrency Control(otevře se v novém okně)PostgreSQL Global Development Group
  2. RFC 9110: HTTP Semantics(otevře se v novém okně)IETF, 2022
  3. ETag - HTTP(otevře se v novém okně)MDN Web Docs
  4. Optimistic concurrency control(otevře se v novém okně)Wikipedia

Související pojmy

Potřebujete to vyřešit v praxi?

Poradíme, jak na to ve vašem projektu

Vysvětlit pojem je jedna věc, navrhnout kolem něj funkční řešení druhá. Ozvěte se a probereme, co dává smysl u vás.