Stránky

pondělí 30. srpna 2010

Pozor na práci s databází ve webovém prostředí

Nedávno jsem řešil opravdu podivný problém s webovou aplikací, která využívala databázi. Z neznámých důvodu docházelo i u velmi jednoduchých databázových operaci k přepočtům. Na vině bylo více vláknové prostředí, které je běžné pro webovou aplikaci




Vysvětlím problém na jednoduchém příkladu (z ukázek jsou vypuštěny transakce kuli zjednodušení)



Show/Hide
[WebMethod]
public PlayResult Play(int stationId)
{
var station = Station.Find(stationId);

//nejaka narocnejsi operace

station.Credit = station.Credit - 5;
station.Save();
}



Show/Hide
[WebMethod]
public void AddCredit(int stationId, int credit)
{
var station = Station.Find(stationId);
station.Credit = station.Credit + credit;
station.Save();
}



vypadá to jako jednoduchý přímočarý kód ale ....

Co když nastane situace, že se zavolá požadavek Play, načte kredit z databáze, začne vykonávat nějakou herní logiku a mezitím se spustí rychlý požadavek (nemusí byt ani rychlejší ale systém ho zrovna zpracuje rychleji) AddCredit přičte kredit a uloží do databáze. Po té se dokončí požadavek AddCredit odečte kredit a uloží. A co se stane? Požadavek play má v paměti načteny starý kredit a při uložení opět přepíše kredit na povodní hodnotu. Jako by k požadavku AddCredit vůbec nedošlo


Při hledaní problému jsem vyzkoušel sem 3 různé možnosti


Zapnout v databázi Izolaci transakci na serializable
toto je opravdu nedostatečné řešení jelikož veškeré požadavky jdou za sebou a v případě nějakého delšího dotazu musí ostatní čekat na dokončení. Navíc databáze nečeká na dokončení, ale vrací chybu.


Použít pesimistické zamykání v databázové vrstvě
Otestoval jsem zamykání pomocí SELECT id FROM stations WHERE id = 3 FOR UPDATE; (takto implementuje pesimistické zamykání nhibernate). Toto řešení nebylo použitelný. Sice update čekal, dokud se neprovede předchozí update, ale nebránilo to načíst do proměnných neplatné hodnoty. Vlákno se pozastavuje pozdě.


Použít synchronizaci vlakem v aplikační vrstvě
Toto řešení komplikuje provoz aplikace v aplikačním clusteru, protože je dost problém uzamykat vlákna mezi více strojena nicméně toto řešení nakonec fungovalo správně



Ukázka řešení


pro synchronizaci jsem použil Mutex (jelikož se jednotlivé worker procesy se v IIS nevidí nelze použít lock )

Aby nedocházelo k výkonnostním problémům je dobré synchronizovat požadavky jenom v rámci jedny stanice (ostatní stanice nemusí čekat na dokončení operace jiné stanice). to to lze vyřešit pomocí jemného parametru který lze v Mutex použít (alespoň v c#)


Show/Hide
[WebMethod]
public PlayResult Play(int stationId)
{
var contexLocker = new Mutex(false, "sync_mutext_" + stationId);
contextLocker.WaitOne();
try{

var station = Station.Find(stationId);

//nejaka narocnejsi operace

station.Credit = station.Credit - 5;
station.Save();
}
catch (Exception)
{
throw;
}
finally
{
contextLocker.Release();
}


}




Show/Hide
[WebMethod]
public void PridejCredit(int stationId, int credit)
{
var contexLocker = new Mutex(false, "sync_mutext_" + stationId);
contextLocker.WaitOne();
try{
var station = Station.Find(stationId);
station.Credit = station.Credit + credit;
station.Save();
}
catch (Exception)
{
throw;
}
finally
{
contextLocker.Release();
}
}



od té doby jsou data 100% správná

Žádné komentáře:

Okomentovat