Kritická sekce v PHP
2.4.2012Tohle 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ář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.
obecne krom anotaci funkci by samotny kod mel byt dostatecne sebepopisny a to bohous splnuje na 100 % :)
PS: a pak jeste jedna vec bylo by asi dobre az tu bude zas neajka php ukazka trosku komentovat kod :-)
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
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 .
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)
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