it-swarm-eu.dev

Zamykání problému se současným DELETE / INSERT v PostgreSQL

To je docela jednoduché, ale jsem zmaten tím, co dělá PG (v9.0). Začneme jednoduchou tabulkou:

CREATE TABLE test (id INT PRIMARY KEY);

a několik řádků:

INSERT INTO TEST VALUES (1);
INSERT INTO TEST VALUES (2);

Pomocí svého oblíbeného dotazovacího nástroje JDBC (ExecuteQuery) připojím dvě okna relace k db, kde tato tabulka žije. Oba jsou transakční (tj. Auto-commit = false). Říkejme jim S1 a S2.

Stejný bit kódu pro každého:

1:DELETE FROM test WHERE id=1;
2:INSERT INTO test VALUES (1);
3:COMMIT;

Nyní to spusťte v pomalém pohybu a provádějte jeden po druhém v oknech.

S1-1 runs (1 row deleted)
S2-1 runs (but is blocked since S1 has a write lock)
S1-2 runs (1 row inserted)
S1-3 runs, releasing the write lock
S2-1 runs, now that it can get the lock. But reports 0 rows deleted. HUH???
S2-2 runs, reports a unique key constraint violation

Nyní to funguje dobře v SQLServeru. Když S2 provede odstranění, ohlásí 1 řádek odstraněn. A pak vložka S2 funguje dobře.

Mám podezření, že PostgreSQL blokuje index v tabulce, kde tento řádek existuje, zatímco SQLServer uzamkne skutečnou hodnotu klíče.

Mám pravdu? Může to být provedeno?

36
DaveyBob

Mat a Erwin mají pravdu a přidávám pouze další odpověď, abych dále rozvedl to, co řekli, způsobem, který se nehodí do komentáře. Jelikož se zdá, že jejich odpovědi neuspokojují všechny, a tam byl návrh, že by měli být konzultováni vývojáři PostgreSQL, a já jsem jeden, budu se zabývat.

Důležitým bodem je, že podle standardu SQL je v rámci transakce probíhající na úrovni izolace transakce READ COMMITTED Omezení, že práce nesouvisejících transakcí nesmí být viditelná. Kdy je práce prováděných transakcí viditelná, závisí na implementaci. Poukazujete na rozdíl v tom, jak se dva produkty rozhodli implementovat. Žádná implementace neporušuje požadavky normy.

Co se děje v PostgreSQL, podrobně:

 S1-1 běhy (1 řádek vymazán) 

Starý řádek je ponechán na svém místě, protože S1 se může stále vrátit zpět, ale S1 nyní drží zámek na řádku, takže jakákoli další relace, která se pokusí upravit řádek, bude čekat, zda se S1 zaváže nebo vrátí zpět. Kterýkoli přečte tabulky může vidět starý řádek, ledaže by se jej pokusili uzamknout pomocí SELECT FOR UPDATE Nebo SELECT FOR SHARE.

 S2-1 běží (ale je blokován, protože S1 má zámek zápisu) 

S2 nyní musí počkat, až uvidí výsledek S1. Pokud by se S1 měl raději vrátit zpět než potvrdit, S2 by vymazal řádek. Všimněte si, že pokud S1 vloží novou verzi před vrácením zpět, nová verze by tam nikdy nebyla z pohledu jakékoli jiné transakce, ani by stará verze nebyla odstraněna z pohledu jakékoli jiné transakce.

 S1-2 běhy (vložen 1 řádek) 

Tento řádek je nezávislý na starém. Pokud by došlo k aktualizaci řádku s id = 1, vztahovala by se stará i nová verze a S2 mohla aktualizovanou verzi řádku odstranit, když se odblokuje. To, že nový řádek má stejné hodnoty jako některý řádek, který existoval v minulosti, neznamená to stejné jako jeho aktualizovaná verze.

 S1-3 běží, uvolňuje zámek pro zápis 

Takže změny S1 přetrvávají. Jeden řádek je pryč. Byl přidán jeden řádek.

 S2-1 běží, nyní, když může získat zámek. Zprávy však byly odstraněny 0 řádků. HUH ??? 

Interně se stane, že existuje ukazatel z jedné verze řádku na další verzi stejného řádku, pokud je aktualizován. Pokud je řádek odstraněn, neexistuje žádná další verze. Když se transakce READ COMMITTED Probudí z bloku při konfliktu zápisu, následuje tento aktualizační řetězec až do konce; pokud řádek nebyl odstraněn a pokud stále splňuje kritéria výběru dotazu, bude zpracován. Tento řádek byl odstraněn, takže dotaz S2 pokračuje dál.

S2 se může nebo nemusí dostat do nového řádku během skenování tabulky. Pokud ano, uvidí, že nový řádek byl vytvořen po spuštění příkazu DELETE S2, a proto není součástí sady řádků, které jsou pro něj viditelné.

Pokud by PostgreSQL restartoval celý příkaz S2 DELETE od začátku s novým snímkem, choval by se stejně jako SQL Server. Komunita PostgreSQL to nezvolila z výkonnostních důvodů. V tomto jednoduchém případě byste si nikdy nevšimli rozdílu ve výkonu, ale pokud jste byli blokováni deset miliónů řádků do DELETE, určitě byste. Zde je kompromis, kde si PostgreSQL zvolil výkon, protože rychlejší verze stále splňuje požadavky normy.

 S2-2 běží, ohlásí porušení jedinečného klíče omezení 

Řada samozřejmě již existuje. Toto je nejméně překvapující část obrázku.

I když zde existuje nějaké překvapivé chování, vše je v souladu s SQL standardem a v mezích toho, co je „specifické pro implementaci“ podle standardu. Určitě to může být překvapivé, pokud předpokládáte, že ve všech implementacích bude přítomno chování jiné implementace, ale PostgreSQL se snaží velmi těžko vyhnout selhání serializace na izolační úrovni READ COMMITTED A umožňuje některá chování, která se liší od jiných produktů. aby toho bylo dosaženo.

Nyní osobně nejsem velkým fanouškem úrovně izolace transakcí READ COMMITTED V implementaci produktu any. Všichni umožňují rasovým podmínkám vytvářet překvapující chování z transakčního hlediska. Jakmile si někdo zvykne na podivné chování povolené jedním produktem, má sklon považovat to „normální“ a kompromisy vybrané jiným produktem za zvláštní. Ale každý produkt musí udělat nějaký kompromis pro jakýkoli režim, který ve skutečnosti není implementován jako SERIALIZABLE. Tam, kde se vývojáři PostgreSQL rozhodli nakreslit čáru v READ COMMITTED, Je minimalizovat blokování (čtení ne blokuje zápisy a zápisy neblokuje čtení) a minimalizuje šanci na selhání serializace.

Standard vyžaduje, aby byly výchozí transakce SERIALIZABLE, ale většina produktů to nedělá, protože to způsobí zásah výkonu přes více laxní úrovně izolace transakcí. Některé produkty neposkytují skutečně serializovatelné transakce, když je vybrána možnost SERIALIZABLE - zejména Oracle a verze PostgreSQL před 9.1. Ale použití skutečně transakcí SERIALIZABLE je jediným způsobem, jak se vyhnout překvapivým účinkům z podmínek závodu, a transakce SERIALIZABLE musí vždy blokovat, aby se zabránilo podmínkám závodu, nebo vrátit některé transakce, aby se zabránilo vyvíjejícím se podmínkám závodu. . Nejběžnější implementací transakcí SERIALIZABLE je přísné dvoufázové zamykání (S2PL), které má selhání blokování i serializace (ve formě zablokování).

Úplné zveřejnění: Spolupracoval jsem s Danem Portsem z MIT=) na přidání skutečně serializovatelných transakcí do PostgreSQL verze 9.1 pomocí nové techniky nazvané Serializable Snapshot Isolation.

40
kgrittn

Věřím, že je to záměrné, podle popisu úrovně izolace potvrzené čtením pro PostgreSQL 9.2:

Příkazy UPDATE, DELETE, SELECT FOR UPDATE a SELECT FOR SHARE se chovají stejně jako SELECT, pokud jde o hledání cílových řádků: , najdou pouze cílové řádky, které byly odevzdány od začátku příkazu 1. Takový cílový řádek však mohl být již aktualizován (nebo smazán nebo uzamčen) jinou souběžnou transakcí v době, kdy je nalezen. V tomto případě bude budoucí aktualizátor čekat, až se první transakce aktualizace potvrdí nebo zruší (pokud stále probíhá). Pokud se první aktualizátor vrátí zpět, jeho účinky jsou negovány a druhý aktualizátor může pokračovat v aktualizaci původně nalezeného řádku. Pokud se první aktualizátor zaváže, druhý aktualizátor ignoruje řádek, pokud jej první aktualizátor odstranil 2, jinak se pokusí použít svou operaci na aktualizovanou verzi řádku.

Řádek, který vložíte do _S1_ ještě neexistoval, když _S2_ se začalo DELETE. Smazání tedy nebude vidět v _S2_ podle (1) výše. Ten, který _S1_ smazán, je ignorován _S2_'sDELETE podle (2).

Takže v _S2_ neodstraní nic. Když ale přijde vložka, ta jedna dělá viz _S1_ 's vložkou:

Protože režim čtení potvrzeného závazku začíná každý příkaz novým snímkem , který zahrnuje všechny transakce potvrzené až do tohoto okamžiku, následující příkazy ve stejné transakci uvidí účinky potvrzené souběžné transakce v každém případě . Výše zmíněným bodem je to, zda jediný příkaz vidí absolutně konzistentní pohled na databázi.

Takže pokus o vložení pomocí _S2_ selhal s porušením omezení.

Pokračování ve čtení tohoto dokumentu pomocí opakovatelné čtení nebo dokonce serializovatelné nevyřeší váš problém úplně - druhá relace selže s chybou serializace při smazání.

To vám však umožní zopakovat transakci.

21
Mat

Úplně souhlasím s @ Matova vynikající odpověď . Napíšu jen další odpověď, protože by se nehodila do komentáře.

Odpověď na váš komentář: DELETE v S2 je již závislý na konkrétní verzi řádku. Protože je to mezitím S1 zabito, považuje se S2 za úspěšný. Ačkoli to není z rychlého pohledu zřejmé, řada událostí je prakticky taková:

 S1 DELETE úspěšné 
 S2 DELETE (úspěšné pomocí proxy - DELETE z S1) 
 S1 znovu vloží odstraněnou hodnotu prakticky mezitím  
 S2 INSERT selže s porušením jedinečného omezení klíče 

Všechno je to záměrně. Skutečně potřebujete použít transakce SERIALIZABLE pro vaše požadavky a ujistěte se, že se pokusíte opakovat selhání serializace.

11
Erwin Brandstetter

Použijte ODLOŽITELNÝ primární klíč a akci opakujte.

0
Frank Heikens