Databázové transakce

This page is a translated version of the page Database transactions and the translation is 100% complete.

Databázové transakce využívá MediaWiki k zachování konzistence databáze a tím i ke zlepšení jejího výkonu.

Některé obecné informace o databázových transakcích naleznete zde:

Rozsah transakce

Nejprve bychom měli rozlišit dva typy metod:

  • S vnějším rozsahem transakce: Metody, které strukturálně jasně zaručují, že nemají žádné volající v řetězci, kteří provádějí transakční operace (kromě zabalení metody okolo transakce jménem uvedené metody). O takových metodách se říká, že vlastní cyklus transakce. Místa, která mají tento rozsah, jsou metoda execute() Skripty údržby, metoda run() tříd Job a metoda doUpdate() tříd DeferrableUpdate. Když jsou tyto metody spuštěny, žádní volající dále v zásobníku volání nebudou mít žádnou transakční aktivitu kromě případného definování počáteční/koncové hranice. O volajícím z vnějšího rozsahu, u kterého je také strukturálně zaručeno, že začne bez deklarovaného cyklu transakce, se říká, že má definující rozsah transakce. To znamená, že metody s vnějším rozsahem transakce jsou volné pro zahájení a ukončení transakcí (s ohledem na některá upozornění popsaná níže). Volající v zásobníku nemají vnější rozsah a očekává se, že tuto skutečnost budou respektovat.
  • S nejasným/vnitřním rozsahem transakce: To jsou metody, u kterých není jasně zaručeno, že mají vnější rozsah transakce. Takové metody nevlastní cyklus transakce, pokud takový existuje. Toto je většina metod v jádru MediaWiki a rozšířeních. Do této kategorie spadají různé metody, jako jsou třídy model/abstrakce, třídy užitných vlastností, objekty DAO, obslužné rutiny háčku, třídy obchodní/kontrolní logiky a tak dále. Tyto metody nesmí volně spouštět/ukončit transakce a musí používat pouze sémantiku transakcí, která podporuje vnořování. Pokud potřebují provést nějaké aktualizace po potvrzení, musí zaregistrovat metodu zpětného volání po potvrzení.

Pokud je kolo transakce zahájeno přes LBFactory::beginPrimaryChanges(), pak se nazývá explicitní kolo transakce. V opačném případě, pokud DBO_TRX zabalí jakoukoli aktivitu dotazu do transakčního cyklu, jak je tomu obvykle během webových požadavků, pak se to nazývá implicitní cyklus transakce. Takové cykly jsou bez vlastníka a provádí je MediaWiki při vypnutí prostřednictvím LBFactory::commitPrimaryChanges. Volající mohou zahájit explicitní cykly uprostřed implicitních cyklů, v takovém případě budou všechny čekající zápisy do databáze potvrzeny, když se explicitní cyklus potvrdí.

Základní použití transakce

MediaWiki používá transakce několika způsoby:

  1. Používání "tradičních" párů begin()/commit() k ochraně kritických sekcí a ujištění, že jsou zavázány. Vnořené transakce nejsou podporovány. Toto by mělo být použito pouze u volajících, kteří mají vnější rozsah transakcí a ovlivňují pouze jednu databázi (přičemž také počítají s všemi možnými obslužnými osobami). Platné metody zahrnují zpětná volání na onTransactionIdle() nebo AutoCommitUpdate, kde je aktualizována pouze jedna DB a nejsou aktivovány žádné háčky. Vždy spojte každý begin() s commit().
  2. Použití párů startAtomic()/endAtomic() k ochraně kritických sekcí, aniž byste věděli, kdy se zadají. Vnořené sekce jsou plně podporovány. Ty lze použít kdekoli, ale musí být správně vnořené (např. neotevírejte sekci a poté ji nezavírejte před příkazem "return"). Ve skriptech údržby, když nejsou otevřené žádné atomické sekce, dojde k potvrzení. Pokud je však nastaven příznak DBO_TRX, atomické sekce se připojí k hlavní sadě transakcí DBO_TRX. Uvnitř zpětných volání AutoCommitUpdate nebo onTransactionIdle() je DBO_TRX pro zadanou databázi vypnuto, což znamená, že endAtomic() se potvrdí, jakmile v těchto zpětných voláních nebudou žádné sekce.
  3. Použití implicitních otočných transakčních cyklů, pokud je povoleno DBO_TRX (toto je výchozí případ u webových požadavků, ale ne pro režim údržby nebo testy jednotek). První zápis při každém připojení databáze bez transakce spustí BEGIN. COMMIT nastane na konci požadavku pro všechna připojení databází s čekajícími zápisy. Pokud je zapsáno více databází, které mají nastaveno DBO_TRX, pak všechny provedou svůj krok potvrzení v rychlém sledu na konci požadavku. To maximalizuje atomičnost transakcí napříč DB. Všimněte si, že optimistická kontrola souběžnosti (REPEATABLE-READ nebo SERIALIZABLE v PostgreSQL) to může poněkud podkopat, protože SERIALIZATION FAILURE může nastat na správné podmnožině odevzdání, i když se zdálo, že všechny zápisy byly úspěšné. V každém případě DBO_TRX snižuje počet potvrzení, což může pomoci výkonu webu (snížením fsync() volání) a znamená, že všechny zápisy v požadavku jsou obvykle buď potvrzeny nebo odvolány společně.
  4. Použití explicitních pivotovaných transakcí zaokrouhluje přes LBFactory::beginPrimaryChanges a LBFactory::commitPrimaryChanges. Tyto cykly jsou účinné v režimu webu i CLI a mají stejnou sémantiku jako jejich implicitní protějšky, s výjimkou následujících aspektů:
    • Volání commitPrimaryChanges() z metody, která nezahájila cyklus, vyvolá chybu.
    • Po dokončení potvrdí všechny prázdné transakce v hlavních databázích a vymažou všechny REPEATABLE-READ snímky. To zajišťuje, že volající, kteří se spoléhají na LBFactory::flushReplicaSnapshots() v nastavení jednoho DB, budou mít stále čerstvé snímky pro připojení na DB_REPLICA. Zajišťuje také, že posluchač transakcí nastavený na Maintenance::setTriggers vidí všechny databáze ve stavu nečinnosti transakce, což mu umožňuje spouštět odložené aktualizace.
  5. Pokud je v kterémkoli okamžiku vyvolána výjimka a není zachycena ničím jiným, MWExceptionHandler ji zachytí a vrátí zpět všechna databázová spojení s transakcemi. To je velmi užitečné v kombinaci s DBO_TRX.

Chyby zneužití transakce

Různá zneužití transakcí způsobí výjimky nebo varování, například:

  • Vnoření volání begin() vyvolá výjimku
  • Volání commit() na transakci jiné metody, která začíná begin(), vyvolá výjimku.
  • Volání begin() nebo commit(), když je atomová sekce aktivní, vyvolá výjimku.
  • Použití LBFactory::beginPrimaryChanges a LBFactory::commitPrimaryChanges má analogická omezení jako výše.
  • Volání commit(), když není otevřena žádná transakce, vyvolá varování.
  • startAtomic() a endAtomic() očekávají __METHOD__ jako argument a jejich hodnota se musí shodovat na každé úrovni vnoření atomových sekcí. Pokud se neshoduje, je vyvolána výjimka.
  • Volání begin() nebo commit(), když je nastaveno DBO_TRX, může zaznamenat varování a neoperaci.
  • Volání rollback(), když je DBO_TRX nastaveno, vyvolá chybu a spustí rollback všech DB.
  • Volání getScopedLockAndFlush(), zatímco v transakci stále čekají zápisy, bude mít za následek výjimku.
  • Zachycení výjimek DBError, DBExpectedError nebo DBQueryError bez volání rollbackPrimaryChanges() může vést k výjimce.
  • Pokus o použití begin() nebo commit() v SqlDataUpdate, která je nastavena na použití podpory transakcí, kterou třída poskytuje, může způsobit výjimky. Tím propadne vnější rozsah, takže více takových aktualizací může být součástí jednoho cyklu transakce.

Vhodné kontexty pro zápisové dotazy

Kromě staršího kódu by se transakce zápisu do databáze (včetně dotazů v režimu automatického potvrzení) v MediaWiki měly dít pouze během provádění:

  • HTTP POST požadavky na SpecialPages, kde ve třídě PHP doesWrites() vrátí hodnotu true
  • HTTP POST požadavky na stránky Action, kde ve třídě PHP vrátí doesWrites() hodnotu true
  • Požadavky HTTP POST na moduly API, kde ve třídě PHP vrací isWriteMode() hodnotu true
  • Úlohy v JobRunner (který používá interní požadavky HTTP POST na webu)
  • Skripty údržby se spouštějí z příkazového řádku

Pro zápisy v kontextu požadavků HTTP GET použijte frontu úloh.

Pro zápisy, ke kterým nemusí dojít před odesláním HTTP odpovědi klientovi, mohou být odloženy přes DeferredUpdates::addUpdate() s příslušnou podtřídou DeferrableUpdate (obvykle AtomicSectionUpdate nebo AutoCommitUpdate) nebo přes DeferredUpdates::addCallableUpdate() se zpětným voláním. Fronta úloh by se měla pro takové aktualizace používat, když jsou pomalé nebo příliš náročné na prostředky, aby se spouštěly v běžných vláknech požadavků na nevyhrazených serverech.

Určení atomové skupiny zápisů

Když je sada dotazů úzce spojena při určování jednotky zápisů databáze, měli bychom použít atomickou sekci. Například:

$dbw->startAtomic( __METHOD__ );
$res = $dbw->select( 'mytable', '*', ..., __METHOD__, [ 'FOR UPDATE' ] );
// determine $rows based on $res
$dbw->insert( 'mysubtable', $rows, __METHOD__ );
$dbw->update( 'mymetatable', ..., ..., __METHOD__ );
$dbw->endAtomic( __METHOD__ );

Dalším způsobem, jak to udělat, je použít doAtomicSection(), což je užitečné, pokud existuje mnoho příkazů return.

$dbw->doAtomicSection(
    __METHOD__,
    function ( IDatabase $dbw ) {
        $res = $dbw->select( 'mytable', '*', ..., __METHOD__, [ 'FOR UPDATE' ] );
        // determine $rows based on $res
        $dbw->insert( 'mysubtable', $rows, __METHOD__ );
        $dbw->update( 'mymetatable', ..., ..., __METHOD__ );
    }
);

Rozdělení zápisů do více transakcí

Zlepšení výkonu

Situace

Předpokládejme, že máte nějaký kód, který aplikuje některé aktualizace databáze. Po dokončení metody můžete chtít:

  • a) Aplikovat některé vysoce sporné aktualizace databáze na konci transakce, aby nedržely uzamčení příliš dlouho
  • b) Aplikovat další aktualizace databáze, které jsou pomalé, neaktuální a nepotřebují 100% atomicitu (např. mohou být obnoveny)

Metody

V některých případech může kód chtít vědět, že data jsou potvrzena, než budete pokračovat k dalším krokům. Jedním ze způsobů, jak toho dosáhnout, je umístit další kroky zpětného volání na onTransactionIdle(), AtomicSectionUpdate nebo AutoCommitUpdate. Poslední dva jsou DeferredUpdates, které se poněkud liší v režimu údržby a požadavků na web/úlohu:

  • Ve webových požadavcích a úlohách (včetně úloh v režimu CLI) se odložené aktualizace spouštějí po potvrzení hlavního kola transakce. Každá aktualizace je zabalena do vlastního transakčního cyklu, i když AutoCommitUpdate zakáže DBO_TRX na zadaném popisovači databáze a odevzdá každý dotaz za běhu. Pokud odložené aktualizace zařadí do fronty další odložené aktualizace, jednoduše se přidají další kola transakcí.
  • Ve skriptech údržby se odložené aktualizace spouštějí po potvrzení jakékoli transakce v lokální (např. "aktuální wiki") databáze (nebo okamžitě, pokud neexistuje žádná otevřená transakce). Odložené aktualizace nelze jednoduše automaticky odložit, dokud nebudou aktivní žádné transakce, protože to může vést k chybám z nedostatku paměti u dlouho běžících skriptů, kde nějaká (možná "cizí wiki") databáze má vždy aktivní transakci (což by jinak bylo ideální). To je důvod, proč jsou odložené aktualizace podivně vázány pouze na místní primární databáze. Bez ohledu na to, protože Maintenance::execute()vnější rozsah transakce a DBO_TRX je pro ně vypnuto, obvykle nedává smysl přímo volat DeferredUpdates::addUpdate() z metody execute(), protože kód by se mohl spustit okamžitě.

Jakákoli metoda s vnějším rozsahem transakcí má možnost zavolat commitPrimaryChanges( __METHOD__ ) na LBFactory singleton k vyprázdnění všech aktivních transakcí ve všech databázích. To zajišťuje, že všechny čekající aktualizace budou potvrzeny před provedením dalších řádků kódu. Také, pokud taková metoda chce zahájit cyklus transakce, může použít beginPrimaryChanges( __METHOD__ ) na singleton, provést aktualizace a pak zavolat commitPrimaryChanges( __METHOD__ ) na singleton. Toto nastaví DBO_TRX na aktuální a nové DB handles během cyklu, což způsobí spuštění implicitních transakcí, dokonce i v CLI režimu. Značka DBO_TRX se po skončení cyklu vrátí do původního stavu. Všimněte si, že podobně jako begin() a commit() nelze vnořit cykly transakcí.

Všimněte si, že některé databáze, jako ty, které zpracovávají ExternalStoreDB, mají obvykle DBO_DEFAULT vypnuté. To znamená, že zůstávají v režimu automatického potvrzení i během transakčních kol.

Příklad

Pro výše uvedené případy uvádíme několik technik, jak je zvládnout:

Případ A:

// Update is still atomic by being in the main transaction round, but is near the end
$dbw->onTransactionIdleOrPreCommit( function () use ( $dbw ) {
    $dbw->upsert( 
        'dailyedits', 
        [ 'de_day' => substr( wfTimestamp( TS_MW ), 0, 6 ), 'de_count' => 1 ],
        [ 'de_day' ],
        [ 'de_count = de_count + 1' ],
        __METHOD__
    );
} );

Případ B:

DeferredUpdate::addUpdate(
    new AtomicSectionUpdate( 
        __METHOD__,
        $dbw,
        function ( $dbw, $fname ) {
        ...set of atomic statements...
        } 
    )
) );
DeferredUpdate::addUpdate(
    new AutoCommitUpdate( 
        __METHOD__,
        $dbw,
        function ( $dbw, $fname ) {
        ...set of autocommit statements...
        } 
    )
) );

Zpracování zpoždění replikace

Situace

Zápis dotazů (např. operace vytvoření, aktualizace, odstranění), které ovlivňují mnoho řádků nebo mají špatné využití indexu, trvá dlouho, než se dokončí. Horší je, že replikované databáze často používají sériovou replikaci, takže aplikují primární transakce jednu po druhé. To znamená, že 10sekundový dotaz UPDATE zablokuje tak dlouho u každé replikované databáze (někdy i více, protože replikové databáze musí zpracovávat provoz čtení a replikovat primární zápisy). To vytváří zpoždění, kdy se jiné aktualizace na primárním serveru chvíli nezobrazují ostatním uživatelům. Také to zpomaluje uživatele provádějící úpravy kvůli ChronologyProtector , který se snaží čekat, až repliky doběhnou.

Hlavní případy, kdy k tomu může dojít, jsou:

  • a) Pracovní třídy, které provádějí nákladné aktualizace
  • b) Skripty údržby, které provádějí hromadné aktualizace velkých částí tabulek

Další situace, která někdy nastane, je, když aktualizace spustí úlohu a tato úloha musí provést nějaké složité dotazy, aby něco přepočítala. Možná není dobrý nápad provádět dotazy na hlavní databázi, ale replikované databáze mohou být zpožděné a neodrážejí změnu, která spustila úlohu. To vede k následujícímu případu:

  • c) Úlohy, které potřebují čekat, až se dokončí jedna replika DB, aby ji mohli dotazovat a určit aktualizace

Podobný scénář může nastat u externích služeb. Předpokládejme, že služba potřebuje provádět nákladné API dotazy, aby odrážela změny v databázi wiki. Takže zbývá další případ:

  • d) Volající, kteří provádějí aktualizace předtím, než upozorní externí službu, že se potřebuje zeptat na DB, aby se aktualizovala

Metody

Náročné aktualizace, které vytvářejí zpoždění, je třeba přesunout do třídy Job a metoda run() úlohy by měla dávkovat aktualizace a čekat, až se repliky dokončí mezi každou dávkou.

Příklady

Případ A / B:

$dbw = wfGetDB( DB_PRIMARY );
$factory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
$ticket = $factory->getEmptyTransactionTicket( __METHOD__ );
$rowBatches = array_chunk( $rows, $wgUpdateRowsPerQuery );
foreach ( $rowBatches as $rowBatch ) {
    $dbw->insert( 'mydatatable', $rows, __METHOD__ );
    ...run any other hooks or methods...
    $factory->commitAndWaitForReplication( __METHOD__, $ticket );
}

Případ C:

$lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
$dbr = $lb->getConnection( DB_REPLICA );
// Wait for $dbr to reach the current primary position
$lb->safeWaitForPrimaryPos( $dbr );
// Clear any stale REPEATABLE-READ snapshots
$dbr->flushSnapshot( __METHOD__ );

$factory->beginPrimaryChanges( __METHOD__ );
...query $dbr and do updates...
$factory->commitPrimaryChanges( __METHOD__ );

Případ D:

$dbw = wfGetDB( DB_PRIMARY );
...do updates to items in $dbw...
// Use a POSTSEND deferred update to avoid blocking the client
DeferredUpdates::addCallableUpdate( 
    function () {
        $factory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
        $factory->commitAndWaitForReplication();
        // Send idempotent HTTP request to regenerate the changed items
        $http = new MultiHttpClient( [] );
        $http->run( ... );
        // Service will do expensive API queries hitting DB_REPLICA
    },
    DeferredUpdates::POSTSEND,
    $dbw // abort callback on rollback
);

Aktualizace sekundárních uložení jiných než RDBMS

Situace

Někdy změny primární datové sady vyžadují aktualizace sekundárních datových úložišť (kterým chybí BEGIN...COMMIT), například:

  • a) Zařadí úlohu, která se bude dotazovat na některé z dotčených řádků, takže koncový uživatel bude čekat na její vložení
  • b) Zařadí do fronty úlohu, která se bude dotazovat na některé z dotčených řádků a vloží ji po vyprázdnění odpovědi MediaWiki koncovému uživateli
  • c) Odešle požadavek službě, která se dotáže na některé z ovlivněných řádků, takže koncový uživatel bude čekat na požadavek služby
  • d) Odešle požadavek službě, která se dotáže na některé z ovlivněných řádků, a to po vyprázdnění odpovědi MediaWiki koncovému uživateli
  • e) Vymaže mezipaměť proxy serveru CDN pro adresy URL, jejichž obsah je založen na ovlivněných řádcích
  • f) Vymaže položku WANObjectCache pro změněný řádek
  • g) Uložení neodvoditelného textu/polostrukturovaného objektu blob do jiného úložiště
  • h) Uložení neodvoditelného souboru do jiného úložiště
  • i) Obslužný program háčku vytvoření účtu vytvářející položku LDAP, která musí nového uživatele doprovázet
  • j) Aktualizace databáze a odeslání e-mailu do schránky uživatele v rámci požadavku uživatele

Metody

Obecně platí, že odvoditelné (např. mohou být regenerovány) aktualizace externích uložení budou používat nějakou třídu DeferrableUpdate nebo onTransactionIdle(), které se použijí po potvrzení. V případech, kdy jsou externí data neměnná, lze na ně odkazovat pomocí autoinkrementačního ID, UUID nebo hash externě uloženého obsahu. V takových případech je nejlepší uložit data předem. Aktualizace, které nespadají do žádné kategorie, by měly používat onTransactionPreCommitOrIdle(), dávkovat všechny aktualizace do externího úložiště do jedné transakce, pokud je to možné, a vyvolat chybu, pokud se aktualizace nezdaří (což spustí vrácení RDBMS). Tím se omezí okno, ve kterém se věci mohou pokazit a vést k nekonzistentním datům.

Příklady

Případ A:

$job = new MyJobClass( $title, [ ... ] );
// Job insertion will abort if $dbw rolls back for any reason
$dbw->onTransactionIdle( function() use ( $jobs ) {
    JobQueueGroup::singleton()->push( $job );
} );

Případ B:

$job = new MyJobClass( $title, [ ... ] );
// Job insertion will abort if $dbw rolls back for any reason
$dbw->onTransactionIdle( function() use ( $jobs ) {
    // End-user is not blocked on the job being pushed
    JobQueueGroup::singleton()->lazyPush( $job );
} );

Případ C:

DeferredUpdate::addCallableUpdate( 
    function () use ( $data ) {
        $http = new MultiHttpClient( [] );
        $http->run( ... );
    },
    DeferredUpdates::PRESEND, // block the end-user
    $dbw // abort update on rollback of this DB
);

Případ D:

DeferredUpdate::addCallableUpdate( 
    function () use ( $data ) {
        $http = new MultiHttpClient( [] );
        $http->run( ... );
    },
    DeferredUpdates::POSTSEND, // don't block end-user
    $dbw // abort update on rollback of this DB
);

Případ E:

DeferredUpdate::addUpdate( 
    new CdnCacheUpdate( $urls ),
    DeferredUpdates::PRESEND // block end-user so they don't see stale pages on refresh
) );

Případ F:

// Update a row
$dbw->update( 'mytable', ..., [ 'myt_id' => $id ], __METHOD__ ); 
// Invalidate the corresponding cache key
$cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
$key = $cache->makeKey( 'my-little-key', $id );
$dbw->onTransactionIdle( function () use ( $cache, $key ) {
    $cache->delete( $key ); // purge/tombstone key right after commit
} );

Případ G:

$dbw = wfGetDB( DB_PRIMARY );
$vrs = MediaWikiServices::getInstance()->getVirtualRESTServiceClient();
// Define the row data
$uuid = UIDGenerator::newUUIDv1();
$row = [ 'myr_text_uuid' => $uuid, ... ];
// First insert blob into the key/value store keyed under $uuid
$status = $vrs->run( [ 'method' => 'PUT', 'url' => "/mystore/map-json/{$uuid}", 'body' => $blob, ... );
if ( !$status->isGood() ) {
   throw new RuntimeException( "Failed to update key/value store." );
}
// Insert record pointing to blob.
// If we fail to commit, then store will just have a dangling blob.
// However, the user will not see records with broken blobs.
$dbw->insert( 'myrecords', $row, __METHOD__ );

Případ H:

$dbw = wfGetDB( DB_PRIMARY );
$be = FileBackendGroup::singleton()->get( 'global-data' );
// Define the row data
$sha1 = $tempFsFile->getSha1Base36(); // SHA-1 of /tmp file uploaded from user
$row = [ 'maf_text_sha1' => $sha1, ... ];
// Make the container/directory if needed
$status = $be->prepare( [ 'dir' => "mwstore://global-data/mytextcontainer" ] );
// Copy the file into the store
$status->merge( $be->store( [ 'src' => $tempFsFile->getPath(), 'dst' => "mwstore://global-data/mytextcontainer/{$sha1}.png" ] ) );
if ( !$status->isGood() ) {
   throw new RuntimeException( "Failed to update key/value store." );
}
// Insert record pointing to file.
// If we fail to commit, then store will just have a dangling file.
// However, the user will not see records with broken files.
$dbw->insert( 'myavatarfiles', $row, __METHOD__ );

Případ I:

// LDAP will not be updated if $dbw rolls back for any reason
$dbw->onTransactionPreCommitOrIdle( function() use ( $ldap ) {
    $status = $ldap->createUser( $user );
    if ( !$status->isGood() ) {
        // If the user already exists or LDAP is down, 
        // throw a GUI error and rollback all databases.
        $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
        $lbFactory->rollbackPrimaryChanges( __METHOD__ );
        throw new ErrorPageError( ... );
    }
    // If COMMIT fails, then we have an LDAP user with no local
    // user row. The code should be able to recover from this.
} );

Případ J:

// Email will not be sent if $dbw rolls back for any reason
$dbw->onTransactionIdle( function() use ( $subject, $body, $user ) {
    $status = $user->sendEmail( $subject, $body );
    if ( !$status->isGood() ) {
        // Assuming this mail is critical, throw an error if it fail to send/enqueue.
        // Many setups will use local queuing via exim and bounces are usually not
        // synchronous, so there is no way to know for sure if the email "made it".
        throw new ErrorPageError( ... );
    }
} );

Použití vrácení transakce

Použití rollback() by se mělo důrazně vyhnout, protože ovlivňuje to, co dělal veškerý předchozí spuštěný kód před vrácením zpět. Vnější volající mohou stále považovat operaci za úspěšnou a pokusit se o další aktualizace a/nebo zobrazit falešnou stránku úspěchu. Jakýkoli snímek REPEATABLE-READ je obnoven, což způsobuje, že objekty s pomalým načítáním pravděpodobně nenajdou to, co hledaly, nebo získají neočekávaně novější data než zbytek toho, co bylo načteno. Použití rollback() je obzvláště špatné, protože jiné databáze mohou mít související změny a je snadné je zapomenout vrátit zpět.

Místo toho stačí vyvolání výjimky ke spuštění vrácení všech databází kvůli MWExceptionHandler::handleException(). Toto pravidlo má dva zvláštní případy:

  • Výjimky typu ErrorPageError (používané pro chyby GUI přívětivé pro člověka) nespouštějí rollback u webových požadavků uvnitř MediaWiki::run(), pokud jsou vyvolány před MediaWiki::doPostOutputShutdown(). To umožňuje akcím, speciálním stránkám a odloženým aktualizacím PRESEND zobrazovat správné chybové zprávy, zatímco nástroje pro audit a ochranu proti zneužití mohou stále zaznamenávat aktualizace do databáze.
  • Volající mohou zachytit výjimky na úrovni DBError, aniž by je znovu vyvolali nebo vyvolali vlastní verzi chyby. To je extrémně špatný postup a může způsobit nejrůznější problémy od částečných odevzdání až po pouhé chrlení chyb DBTransactionError. Chyby DB zachyťte pouze proto, abyste provedli nějaké vyčištění před opětovným vyvoláním chyby nebo v případě, že je daná databáze používána výhradně kódem zachycujícím chyby.

Takto se normálně používá vrácení zpět, jako zabezpečení proti selhání, které vše přeruší, vrátí se do výchozího stavu a dojde k chybám. Pokud je však skutečně potřeba přímé volání rollback, vždy použijte rollbackPrimaryChanges() na LBFactory singleton, abyste se ujistili, že se všechny databáze vrátí do původního stavu jakéhokoli transakčního cyklu.

Protokolování ladění

K protokolování chyb a varování souvisejících s DB se používá několik kanálů (skupin protokolů):

  • DBQuery
  • DBConnection
  • DBPerformance
  • DBReplication
  • exception

Na Wikimedii lze tyto protokoly nalézt dotazem na logstash.wikimedia.org pomocí +channel:<NÁZEV KANÁLU>.

Staré diskuse

Toto je výsledek nějaké konverzace na wikitech-l mailing listu a následné diskuze na Bugzille. Některé relevantní diskuse jsou:

V jednom mailu Tim Starling vysvětlil důvody systému DBO_TRX. Zde je upravená verze jeho vysvětlení:

DBO_TRX poskytuje následující výhody:

  • Poskytuje zlepšenou konzistenci operací zápisu pro kód, který nezná transakce, například rollback-on-error.
  • Poskytuje snímek pro konzistentní čtení, což zlepšuje správnost aplikace při souběžných zápisech.

DBO_TRX byl představen, když jsme přešli na InnoDB, spolu se zavedením Database::begin() a Database::commit()

[...]

Zpočátku jsem nastavil schéma, kde byly transakce "vnořené", v tom smyslu, že begin() zvýšil úroveň transakce a commit() ji snížil. Když byl snížen na nulu, byl vydán skutečný COMMIT. Takže byste měli sekvenci volání jako:

  • begin() -- posílá BEGIN
  • begin() -- nic nedělá
  • commit() -- nic nedělá
  • commit() -- posílá COMMIT

Toto schéma se brzy ukázalo jako nevhodné, protože se ukázalo, že nejdůležitější pro výkon a správnost je, aby aplikace byla schopna po dokončení nějakého konkrétního dotazu potvrdit aktuální transakci. Database::immediateCommit() byl představen na podporu tohoto případu použití -- jeho funkcí bylo okamžité snížení úrovně transakce na nulu a potvrzení podkladové transakce.

Když bylo zřejmé, že každý Database::commit() call by měl být opravdu Database::immediateCommit(), změnil jsem sémantiku a fakticky jsem přejmenoval Database::immediateCommit() na Database::commit(). Odstranil jsem myšlenku vnořených transakcí ve prospěch modelu kooperativního řízení délky transakcí:

  • Database::begin() se stal fakticky ne-operací pro webové požadavky a někdy byl pro stručnost vynechán.
  • Database::commit() by měl být volán po dokončení sekvence operací zápisu, kde je požadována atomičnost, nebo při nejbližší příležitosti, když jsou drženy sporné zámky.

[...]

Když jsou transakce příliš dlouhé, narazíte na problémy s výkonem kvůli sporům o zámek. Když jsou transakce příliš krátké, narazíte na problémy s konzistencí, když požadavky selžou. Schéma, které jsem zavedl, upřednostňuje výkon před konzistentností. Řeší konflikty mezi volajícími a volanými pomocí nejkratšího času transakce. Myslím, že to byla vhodná volba pro Wikipedii, jak tehdy, tak i dnes, a myslím si, že je pravděpodobně vhodná i pro mnoho dalších středně až vysoce provozovaných wiki.

Body záchrany nebyly v době zavedení systému k dispozici. Jsou však zdokonalením opuštěného schématu vnořování transakcí, nikoli zdokonalením současného schématu, které je optimalizováno pro omezení sporů o zámek.

Pokud jde o výkon, možná by bylo možné použít krátké transakce s explicitním begin() s body uložení pro vnoření. Ale pak byste přišli o výhody konzistence DBO_TRX, které jsem zmínil na začátku tohoto příspěvku.

-- Tim Starling