Kritická sekce v PHP

2.4.2012

Tohle bych skoro vůbec nikdy nepotřeboval až do minulého týdne, kdy se nám v rezervačním systému pro Dopravní podnik města Liberce a Jablonce divně zapsala data do databáze. Došlo k tomu při spuštění stejného skriptu velmi rychle za sebou z různých prohlížečů. Docela se bojím, kde všude jinde může tento problém také nastat. Pro zajištění správného běhu programu jsem musel zajistit, aby stejný skript nemohl běžet vícekrát v jeden okamžik. Obdobou tohoto problému v programování pro desktop je zajištění kritické sekce při běhu více vláken (threadů).

Myslíte si, že se vás tento případ netýká? Tak si představte, co se stane s nějakým vaším skriptem, který operuje s databází a ten spustíte v jedné vteřině vícekrát z odlišných počítačů/prohlížečů. Věřím, že nemalé procento skriptů se nezachová přesně tak, jak by mělo. K chybě třeba dojde, když si z databáze načtete nějaký seznam položek, pak něco počítáte a nakonec vytvoříte nějaké nové položky. V případě vícenásobného spuštění může dojít k násobnému insertu stejné položky místo toho, aby byl proveden jen ten první insert. U databáze to lze řešit transakcemi nebo unikátními klíči. Ale jak to řešit, když zrovna nepracujeme s databází nebo transakce a unikátní klíče nelze použít? Sdílení informací mezi jednotlivými requesty se v PHP řeší docela problematicky. Můžeme použít semafory, sdílenou paměť, memcache, ale to jsou dodatečné funkce, které nebývají standardně zapnuty nebo jsou dokonce funkční jen v některých operačních systémech.

Pro vymezení kritické sekce nám ale stačí mnohem jednodušší funkce a to zamykání souborů. Jak Windows, tak Linux umí označit soubor jako uzamčený a to pozastaví běh ostatních skriptů, dokud zamykací skript soubor opět neodemkne. Jednoduché, ale kupodivu funkční i na starších verzích PHP. Když váš skript nečekaně umře uprostřed kritické sekce, tak PHP odemkne všechny uzamčené soubory za vás, protože váš skript už neběží.

Takto by vypadalo použití:

<?php
   CriticalStart('import_zbozi');
   sleep(10); // hodne selectu a insertu, toto pobezi urcite jen jednou
   CriticalEnd('import_zbozi');
?>

A zde funkce pro obsluhu kritické sekce:

<?php  
   function CriticalStart($name){
      global $CriticalSectionsFiles;
      if (!function_exists('sys_get_temp_dir')) {
         if (file_exists('/tmp/')) {
           $tempdir='/tmp/';
        }
        else die("Set temporary directory");
        //$tempdir=
     } else {
        $tempdir=sys_get_temp_dir();
     }
     if (!preg_match('/[\\\\\/]$/',$tempdir)) $tempdir.=DIRECTORY_SEPARATOR;
        $filename=$tempdir.'critical-section-'.md5($name);
        if (!file_exists($filename)) {
            $fp=fopen($filename,'w');
            fputs($fp,"Lock file for: ".$name."\n");
            fclose($fp);
        }
        $fp = fopen($filename,"r"); 
        $lock = flock($fp,LOCK_EX);
        $CriticalSectionsFiles[md5($name)]=$fp;
        return $lock?true:false;
    }
    
    function CriticalEnd($name){
        global $CriticalSectionsFiles;
        if (!$CriticalSectionsFiles[md5($name)]) return true;
        $fp=$CriticalSectionsFiles[md5($name)];
        $lock = flock($fp,LOCK_UN);
        fclose($fp);
        return $lock?true:false;
    }
?>

Jak to je s rychlostí, to vám nepovím, ale zas tak pomalé to snad nebude, protože při zamýkání nedochází k zápisu dat na disk, ale vše se odehrává v paměti systému (alespoň doufám :-)). Největším problém, ale stále zůstává a to uvědomit si při programování, že byste to měli ochránit proti vícenásobnému spuštění.

Setkali jste se s podobným problémem? Jak byste to řešili vy?

Komentáře

Přidej svůj komentář

Přidání komentáře

13.4.2012 18:15 - Bogan

Ten zpusob pres databazi neni spatny, a dokonce si to vlasten mzuete pres to monitorovat a vedet, ze zrovna neno bezi...takovej processlist.

Pokud potrebujes tenhle kod okomentovat, tak ses prd programator :-). Diky za pripominku, jeste to doplnim.

11.4.2012 14:55 - Method

obecne krom anotaci funkci by samotny kod mel byt dostatecne sebepopisny a to bohous splnuje na 100 % :)

9.4.2012 16:23 - [ZAZA]

PS: a pak jeste jedna vec bylo by asi dobre az tu bude zas neajka php ukazka trosku komentovat kod :-)

9.4.2012 16:22 - [ZAZA]

U nas ve firme se to dela trosku jinym zpusobem. Jedna se o to, ze mame x backend scriptu, pro ktere plati pravidlo, ze muze byt spusteny POUZE jeden backend script, ktery trva dejme tomu 1500 sec, ale z nejakeho duvodu se opozdi a trva dvakrat dele a resime to tzv. JobCronem, ktery se stara o to jestli script jeste bezi nebo nepokud ano nepusti se zadny a pak se posilaji nejaky zpravy. Tedy resime to tak ze zapisujeme priznaky do tabulky v DB, RUN => FALSE | TRUE, ale tohle se tyka pouze nejakyho senamu backend scriptu pro jeden to asi nema smysl

3.4.2012 19:16 - Bogan

S tou paranoiou (to je teda slovo) mas asi pravdu, ale az se ti to stane a nebudes vedet co s tim, jeste rad tuhle stranku navstivis Usmívající se.

2.4.2012 22:04 - Method

nebo by se to dalo resit pres SQLite, ktere ma nativni ovladace v PHP. je to vlastne to same, jako jsi tu napsal. SQLite je vlastne 1 souborova databaze, ktera se zamkne, pokud sni pracuje uzivatel.ALE mel bys tam moznost ulozit si treba nejaky mezivypoctovy data ci plne funkce SQL (neumi snad jen right joiny a alter table)

2.4.2012 21:59 - Method

vetsinou kdyz nevim, tak s tim jdu za tebou :) tohle muze byt celkem paranoia, ale pokud se to skutecne stalo, pres zamek na souborech mi to prijde fajn, je to totez jako v transakcich