Hiho,
ich hätte gerne mal eure Meinung dazu gewusst:
Momentan habe ich einige schwere Bugs in meinem Browsergame "Aysalia Arena". Und zwar bekommt man in Tabellen negative Werte, obwohl das laut meinem Programmcode gar nicht möglich sein sollte. Es scheint aber nur sporadisch aufzutreten, mal häufiger (bei mehreren Usern), mal ein paar Tage überhaupt nicht. Bin den COde schon öfter durchgegangen und habe genau das gemacht, was die User laut Log-Datei oder auch laut Selbstaussage getan haben.
Ich selber hatte das Problem trotzdem nie. Habe gestern gestern nun in der entsprechenden Tabelle die Befehle OPTIMIZE und REPAIR durchgeführt. Bis jetzt ist der Fehler noch nicht wieder aufgetreten.
Kann das denn wirklich an der Datenbank liegen oder MUSS zwingend ein Fehler in meinem Code vorliegen?
Problem mit Datenbank??
gepostet vor 17 Jahre, 1 Monat von azamaroth
gepostet vor 17 Jahre, 1 Monat von Eckstoss
Vielleicht eine Sicherheitslücke???
Bisher hatte ich solche Vorkommnisse noch nicht.
Bisher hatte ich solche Vorkommnisse noch nicht.
gepostet vor 17 Jahre, 1 Monat von Drezil
ich tippe mal ganz einfach auf eine race-condition in deinem code.
mehr infos in der wikipedia:
de.wikipedia.org/wiki/Race-Condition
lösen tut man sowas durch locks. man kann das ganz hart mit file-locks machen, oder anfragen in transaktionen packen und eine transaktionssichere datenbank nutzen (wie z.b. innodb innerhalb von mysql oder man nimmt gleich ne ordentliche db wie postgres, oracle oder mssql).
hth
mehr infos in der wikipedia:
de.wikipedia.org/wiki/Race-Condition
lösen tut man sowas durch locks. man kann das ganz hart mit file-locks machen, oder anfragen in transaktionen packen und eine transaktionssichere datenbank nutzen (wie z.b. innodb innerhalb von mysql oder man nimmt gleich ne ordentliche db wie postgres, oracle oder mssql).
hth
gepostet vor 17 Jahre, 1 Monat von TheUndeadable
> Kann das denn wirklich an der Datenbank liegen
Du kannst davon ausgehen, dass solche Datenbankfehler in einem stable-Release nicht vorkommen. Dies wäre schon öfter aufgefallen und würde meiner Meinung nach die Nichteinsetzbarkeit der Datenbankversion bedeuten.
Ich vermute eher, dass du ein paar Vorzeichen nicht beachtest und dementsprechend beabsichtigt oder unbeabsichtigt Additionen mit negativen Summanden machst.
Oder du hast eine Wettlauf-Bedingung:
Spieler A bucht 10 bei X ab
Spieler B bucht 10 bei X ab.
Es wird von 2 Threads gleichzeitig geprüft ob X > 10 Ressourcen hat. Aber es wird dann gleichzeitig 2x 10 abgebucht obwohl der Spieler nur 15 Ressourcen hat.
NACHTRAG:
Ich sehe Drezil war schneller....
Du kannst davon ausgehen, dass solche Datenbankfehler in einem stable-Release nicht vorkommen. Dies wäre schon öfter aufgefallen und würde meiner Meinung nach die Nichteinsetzbarkeit der Datenbankversion bedeuten.
Ich vermute eher, dass du ein paar Vorzeichen nicht beachtest und dementsprechend beabsichtigt oder unbeabsichtigt Additionen mit negativen Summanden machst.
Oder du hast eine Wettlauf-Bedingung:
Spieler A bucht 10 bei X ab
Spieler B bucht 10 bei X ab.
Es wird von 2 Threads gleichzeitig geprüft ob X > 10 Ressourcen hat. Aber es wird dann gleichzeitig 2x 10 abgebucht obwohl der Spieler nur 15 Ressourcen hat.
NACHTRAG:
Ich sehe Drezil war schneller....
gepostet vor 17 Jahre, 1 Monat von Macavity
oder du hast einen dieser typischen Fehler in der Art von "Wenn man was verschenkt gibt man natürlich einen positiven Wert an, da würde niemand einen negative Zahl reinschreiben"
gepostet vor 17 Jahre, 1 Monat von raufaser
Machst du es so:
SQL:
SELECT wert FROM ...
PHP:
wert_neu = wert + xyz
SQL:
UPDATE xxx SET wert='wert_neu'
Oder machst du:
SQL:
UPDATE xxx SET wert=wert+xyz
Variante 1 ist sehr problematisch...
Ich glaube übrigens nicht, dass es an der DB liegt, sondern an deinem Code.
Gruß,
Marc
SQL:
SELECT wert FROM ...
PHP:
wert_neu = wert + xyz
SQL:
UPDATE xxx SET wert='wert_neu'
Oder machst du:
SQL:
UPDATE xxx SET wert=wert+xyz
Variante 1 ist sehr problematisch...
Ich glaube übrigens nicht, dass es an der DB liegt, sondern an deinem Code.
Gruß,
Marc
gepostet vor 17 Jahre, 1 Monat von knalli
Oder ganz genauer:
"Geld verschicken":
UPDATE spieler SET geld=geld-$x WHERE geld >= $x AND spieler_id=$y
Sollte das Query eigentlich nur einmal funktionieren; deswegen anschließend über "affected rows" abfragen, ob eine Zeile (und wirklich nur eine verdammte Zeile) aktualisiert wurden, denn wenn nicht: gibts den Spieler unter dieser ID nicht oder er hat zuwenig Geld (bsp. auch weil die Seite 2x geladen wurde).
"Geld verschicken":
UPDATE spieler SET geld=geld-$x WHERE geld >= $x AND spieler_id=$y
Sollte das Query eigentlich nur einmal funktionieren; deswegen anschließend über "affected rows" abfragen, ob eine Zeile (und wirklich nur eine verdammte Zeile) aktualisiert wurden, denn wenn nicht: gibts den Spieler unter dieser ID nicht oder er hat zuwenig Geld (bsp. auch weil die Seite 2x geladen wurde).
gepostet vor 17 Jahre, 1 Monat von Drezil
ob man den ganzen kram in 1 oder in 100 anfragen macht, ist bezüglich race-conditions unerheblich - lediglich die wahrscheinlichkeit des auftretens wird durch die längere dauer der 100 erhöht.
auf der sicheren seite ist man nur mit transaktionen innerhalb derer man die eingehenden daten prüft und dann umsetzt.
Konkret (bester pseudocode ):
begin transaction;
select * from freie_felder
if (freie_felder > 0)
insert into bau (...)
else
rollback; & die;
update freie_felder set ...
commit;
ich verstehe auch nicht, was an raufasers Variante2 so viel toller sein soll. wenn ich dasselbe script 2x parallel laufen hab, dann kann es immernoch doppelt ausgeführt werden.
Und wenn sich dies wirklich als das problem des OP erweisen sollte, dann hat er wirklich ein großes problem. Bei mir hat diese problematik zu einem kompletten rewrite geführt. man kann sowas nämlich so gut wie nie "im nachhinein" draufsetzen. das ist so wie "und zum schluss machen wir unser programm bugfrei und komplett sicher".
auf der sicheren seite ist man nur mit transaktionen innerhalb derer man die eingehenden daten prüft und dann umsetzt.
Konkret (bester pseudocode ):
begin transaction;
select * from freie_felder
if (freie_felder > 0)
insert into bau (...)
else
rollback; & die;
update freie_felder set ...
commit;
ich verstehe auch nicht, was an raufasers Variante2 so viel toller sein soll. wenn ich dasselbe script 2x parallel laufen hab, dann kann es immernoch doppelt ausgeführt werden.
Und wenn sich dies wirklich als das problem des OP erweisen sollte, dann hat er wirklich ein großes problem. Bei mir hat diese problematik zu einem kompletten rewrite geführt. man kann sowas nämlich so gut wie nie "im nachhinein" draufsetzen. das ist so wie "und zum schluss machen wir unser programm bugfrei und komplett sicher".
gepostet vor 17 Jahre, 1 Monat von raufaser
Original von Drezil
ich verstehe auch nicht, was an raufasers Variante2 so viel toller sein soll. wenn ich dasselbe script 2x parallel laufen hab, dann kann es immernoch doppelt ausgeführt werden.
Was du immer so vermutest... Kern meiner Aussage war, das Variante 1 ganz beschissen ist. Zu Variante 2 habe ich nichts gesagt. Anstatt hier ins blaue hineinzuvermuten und mit Transaktionen um sich zu werfen, finde ich wichtiger erstmal zu erfahren, wie azamaroth es nun genau macht.
Erst dann kann ihm doch geholfen werden, oder sehe ich das falsch?
(Ist jetzt nicht böse gemeint oder so... ich mags nur nicht, wenn zuviel Interpretiert wird, ohne konkretes zu wissen)
Gruß,
Marc
gepostet vor 17 Jahre, 1 Monat von Todi42
Um auszuschließen, dass es keine Race Condition ist, solltest Du mal versuchen, Deinen Web-Server so zu konfigurieren, dass er nur eine Anfrage zur Zeit beantwortet.
gepostet vor 17 Jahre, 1 Monat von Dunedan
Hm, negative Zahlen klingt für mich auf Anhieb erstmal nach einem overflow. Aber kein Ahnung in wie weit das in der speziellen Situation möglich ist.
gepostet vor 17 Jahre, 1 Monat von azamaroth
Also der komplette Code, wenn man ein Item bekommt, sieht folgendermaßen aus:
$result = mysql_query("SELECT * FROM AO_backpack WHERE charname = '$name' AND item = '$artikel'", $db);
$gefunden = mysql_num_rows($result);
if ($gefunden>0) {
$result = mysql_query("UPDATE AO_backpack SET menge = menge + $art_anz WHERE charname = '$name' AND item = '$artikel'", $db);
}
if ($gefunden==0) {
$result = mysql_query("SELECT * FROM AO_backpack WHERE charname = '$name' AND item = '' AND menge = 0 ORDER BY platz ASC", $db);
$leere = mysql_num_rows($result);
if ($leere == 0) {
$einlagern = $artikel;
$mp_menge = $art_anz;
$freier_gegenstand = 1;
echo "
Der Gegenstand wurde in der Bank abgelegt, da dein Rucksack voll ist!
";
$m_id = 0;
include "ao_bank_putin.php";
} else {
$suchen[1]=mysql_fetch_object($result);
$rslot = $suchen[1]->platz;
$result = mysql_query("UPDATE AO_backpack SET item = '$artikel', menge = $art_anz WHERE charname = '$name' AND platz = $rslot", $db);
}
}"
Hierbei ist es schon vorgekommen, dass kein Item inzugefügt wurde.
Aber öfter kommt es vor, dass bei folgendem Code, wenn man ein Item abgibt (vom Rucksack aus auf dem Marktplatz einstellt z.B.), ein negativ Wert entsteht.
if ($entfern_menge>0){
$result = mysql_query("UPDATE AO_backpack SET menge = menge-$entfern_menge WHERE charname = '$name' AND item = '$item_entfernen'", $db);
}
else {
$result = mysql_query("UPDATE AO_backpack SET item = '', menge = 0 WHERE charname = '$name' AND item = '$item_entfernen'", $db);
}
Komischerweise ist dieser Fehler oft nach 0:00 aufgetreten. Und seltener zu anderen Zeiten, aber das kann natürlich Zufall sein, oder Gewohnheit der User
Ich hoffe ich habe genügend Infos gegeben. Ich denke auch dass man an dem Code einiges verbessern kann...
$result = mysql_query("SELECT * FROM AO_backpack WHERE charname = '$name' AND item = '$artikel'", $db);
$gefunden = mysql_num_rows($result);
if ($gefunden>0) {
$result = mysql_query("UPDATE AO_backpack SET menge = menge + $art_anz WHERE charname = '$name' AND item = '$artikel'", $db);
}
if ($gefunden==0) {
$result = mysql_query("SELECT * FROM AO_backpack WHERE charname = '$name' AND item = '' AND menge = 0 ORDER BY platz ASC", $db);
$leere = mysql_num_rows($result);
if ($leere == 0) {
$einlagern = $artikel;
$mp_menge = $art_anz;
$freier_gegenstand = 1;
echo "
Der Gegenstand wurde in der Bank abgelegt, da dein Rucksack voll ist!
";
$m_id = 0;
include "ao_bank_putin.php";
} else {
$suchen[1]=mysql_fetch_object($result);
$rslot = $suchen[1]->platz;
$result = mysql_query("UPDATE AO_backpack SET item = '$artikel', menge = $art_anz WHERE charname = '$name' AND platz = $rslot", $db);
}
}"
Hierbei ist es schon vorgekommen, dass kein Item inzugefügt wurde.
Aber öfter kommt es vor, dass bei folgendem Code, wenn man ein Item abgibt (vom Rucksack aus auf dem Marktplatz einstellt z.B.), ein negativ Wert entsteht.
if ($entfern_menge>0){
$result = mysql_query("UPDATE AO_backpack SET menge = menge-$entfern_menge WHERE charname = '$name' AND item = '$item_entfernen'", $db);
}
else {
$result = mysql_query("UPDATE AO_backpack SET item = '', menge = 0 WHERE charname = '$name' AND item = '$item_entfernen'", $db);
}
Komischerweise ist dieser Fehler oft nach 0:00 aufgetreten. Und seltener zu anderen Zeiten, aber das kann natürlich Zufall sein, oder Gewohnheit der User
Ich hoffe ich habe genügend Infos gegeben. Ich denke auch dass man an dem Code einiges verbessern kann...
gepostet vor 17 Jahre, 1 Monat von knalli
Original von knalli
Oder ganz genauer:
"Geld verschicken":
UPDATE spieler SET geld=geld-$x WHERE geld >= $x AND spieler_id=$y
Sollte das Query eigentlich nur einmal funktionieren; deswegen anschließend über "affected rows" abfragen, ob eine Zeile (und wirklich nur eine verdammte Zeile) aktualisiert wurden, denn wenn nicht: gibts den Spieler unter dieser ID nicht oder er hat zuwenig Geld (bsp. auch weil die Seite 2x geladen wurde).
Ich zitier' mich mal selbst..
also
// ADDIEREN
$result = mysql_query("SELECT * FROM AO_backpack WHERE charname = '$name' AND item = '$artikel'", $db);
// @TODO * in spaltennamen ersetzen
// @TODO $name und $artikel gesichert?
$gefunden = mysql_num_rows($result);
if ($gefunden>0) {
$result = mysql_query("UPDATE AO_backpack SET menge = menge + $art_anz WHERE charname = '$name' AND item = '$artikel'", $db);
// @TODO $name und $artikel gesichert?
// @TODO $art_anz gesichert?
}
Wäre das jetzt ein abziehen (und nur dort können negative Ergebnisse kommen, denn ich denke mal, dass $art_anz>0 ist, prüfst du vorher!):
// SUBTRAHIEREN
$result = mysql_query("SELECT * FROM AO_backpack WHERE charname = '$name' AND item = '$artikel'", $db);
// @TODO * in spaltennamen ersetzen
// @TODO $name und $artikel gesichert?
$gefunden = mysql_num_rows($result);
if ($gefunden>0) {
$result = mysql_query("UPDATE AO_backpack SET menge = menge - $art_anz WHERE menge >= $art_anz AND charname = '$name' AND item = '$artikel'", $db);
// @TODO $name und $artikel gesichert?
// @TODO $art_anz gesichert?
}
Die Queries würd ich bei der Parameteranzahl mit sprintf bauen, also so:
$sql = sprintf("UPDATE AO_backpack SET menge = menge - %1\$d WHERE menge >= %1\$d AND charname='%2\$s' AND item='%3\$s';", (int) $art_anz, f($name), f($artikel));
mit f hab ich mal abgekürzt, dass es bei strings (davon geh ich mal aus, wäre natürlich als id besser und dann natürlich $d und nicht $s heißt) noch eine sicherung mit de2.php.net/manual/de/function.mysql-real-escape-string.php sinnvoll wäre..
Das eine oder andere könnt' man sich sparen oder anders verpacken, dafür kenne ich den Code nicht genau und hab jetzt einfach kleine Mutmaßungen gemacht.
edit: Versuche die Queries so aufeinanderfolgend wie möglich zu bauen (bei Nicht(!)-Verwendung von Transactions..
gepostet vor 17 Jahre, 1 Monat von TheUndeadable
Der 12 Uhr-Lag ist ein Hinweis darauf, dass du eine Wettlaufbedingung hast. Dort hat der Server meistens organisatorische Dinge zu tun und führt die Skripte langsamer aus.
"UPDATE AO_backpack SET menge = menge-$entfern_menge [...]";
Was ist, wenn $entfern_menge größer als menge ist? Dann wird es negativ.
Wie wird $entfern_menge größer als menge? Ganz einfach:
User klickt zweimal schnell auf 'auspacken'. Es werden vom Webserver 2 Skripte gestartet, diese laufen im Thread A und B ab.
Thread A: SELECT menge
Thread A: überprüfe ob menge > $entfern_menge. Passt
Thread B: SELECT menge
Thread B: überprüfe ob menge > $entfern_menge. Passt
Thread A: UPDATE SET menge -= $entfern_menge
Thread B: UPDATE SET menge -= $entfern_menge
=> Es wurde 2 mal $entfern_menge abgezogen, obwohl nur garantiert wurde, dass diese Menge nur einmal im Rucksack vorhanden ist.
> Versuche die Queries so aufeinanderfolgend wie möglich zu bauen
Das reduziert nur die Gefahr einer Wettlaufbedingung, behebt aber das Problem nicht.
"UPDATE AO_backpack SET menge = menge-$entfern_menge [...]";
Was ist, wenn $entfern_menge größer als menge ist? Dann wird es negativ.
Wie wird $entfern_menge größer als menge? Ganz einfach:
User klickt zweimal schnell auf 'auspacken'. Es werden vom Webserver 2 Skripte gestartet, diese laufen im Thread A und B ab.
Thread A: SELECT menge
Thread A: überprüfe ob menge > $entfern_menge. Passt
Thread B: SELECT menge
Thread B: überprüfe ob menge > $entfern_menge. Passt
Thread A: UPDATE SET menge -= $entfern_menge
Thread B: UPDATE SET menge -= $entfern_menge
=> Es wurde 2 mal $entfern_menge abgezogen, obwohl nur garantiert wurde, dass diese Menge nur einmal im Rucksack vorhanden ist.
> Versuche die Queries so aufeinanderfolgend wie möglich zu bauen
Das reduziert nur die Gefahr einer Wettlaufbedingung, behebt aber das Problem nicht.
gepostet vor 17 Jahre, 1 Monat von azamaroth
Original von TheUndeadable
Der 12 Uhr-Lag ist ein Hinweis darauf, dass du eine Wettlaufbedingung hast. Dort hat der Server meistens organisatorische Dinge zu tun und führt die Skripte langsamer aus.
"UPDATE AO_backpack SET menge = menge-$entfern_menge [...]";
Was ist, wenn $entfern_menge größer als menge ist? Dann wird es negativ.
Wie wird $entfern_menge größer als menge? Ganz einfach:
Ist $entfern_menge <= 0, dann wird der Tabelleneintrag gelöscht. Da $entfern_menge 0 bedeutet, dass alles abgegeben wurde. ALso dürfte da kein negativer Wert entstehen, auch wenm $entfern_menge bsp. "-5" ist.
User klickt zweimal schnell auf 'auspacken'. Es werden vom Webserver 2 Skripte gestartet, diese laufen im Thread A und B ab.
Thread A: SELECT menge
Thread A: überprüfe ob menge > $entfern_menge. Passt
Thread B: SELECT menge
Thread B: überprüfe ob menge > $entfern_menge. Passt
Thread A: UPDATE SET menge -= $entfern_menge
Thread B: UPDATE SET menge -= $entfern_menge
=> Es wurde 2 mal $entfern_menge abgezogen, obwohl nur garantiert wurde, dass diese Menge nur einmal im Rucksack vorhanden ist.
> Versuche die Queries so aufeinanderfolgend wie möglich zu bauen
Das reduziert nur die Gefahr einer Wettlaufbedingung, behebt aber das Problem nicht.
Also das habe ich auch getestet, aber da ist auch nichts passiert. Auch mit unterschiedlichen Drück-Intervallen (ganz schnell, schnell, normal) kann ich den Fehler nicht rekonstruieren. Aber ich selber spiele selten mein Spiel um 0 Uhr, um diese Zeit habe ich es noch nie ausprobiert...
gepostet vor 17 Jahre, 1 Monat von TheUndeadable
> Also das habe ich auch getestet, aber da ist auch nichts passiert.
Wenn du kein 'Locking' hast und es theoretisch vorkommen kann, dass eine Wettlaufbedingung entstehen kann, dann entsteht diese früher oder später. Da kannst du auch eine Stunde lang drücken, das bedeutet nichts! Eine Wettlaufbedingung ist ein zufälliges Ereignis und Wahrscheinlichkeiten kann man nicht erzwingen.
Versuche deinen Code transaktionssicher zu machen. Dazu gibt es verschiedene Verfahren.
> Da $entfern_menge 0 bedeutet, dass alles abgegeben wurde.
menge = menge - 0 = menge und nicht 0....
Du kannst natürlich den schwarzen Peter auf die Datenbank schieben, das erscheint erstmal einfacher.
Ansonsten klinke ich mich erstmal aus dem Thread aus.
Wenn du kein 'Locking' hast und es theoretisch vorkommen kann, dass eine Wettlaufbedingung entstehen kann, dann entsteht diese früher oder später. Da kannst du auch eine Stunde lang drücken, das bedeutet nichts! Eine Wettlaufbedingung ist ein zufälliges Ereignis und Wahrscheinlichkeiten kann man nicht erzwingen.
Versuche deinen Code transaktionssicher zu machen. Dazu gibt es verschiedene Verfahren.
> Da $entfern_menge 0 bedeutet, dass alles abgegeben wurde.
menge = menge - 0 = menge und nicht 0....
Du kannst natürlich den schwarzen Peter auf die Datenbank schieben, das erscheint erstmal einfacher.
Ansonsten klinke ich mich erstmal aus dem Thread aus.
gepostet vor 17 Jahre, 1 Monat von Nightflyer
UPDATE `tabelle`SET `myint`=myint-$sub WHERE myint-$sub>0 && `id`=$id
Damit hat man die Prüfung nochmals und eine race-condition kannausgeschlossen werden. HF
Damit hat man die Prüfung nochmals und eine race-condition kannausgeschlossen werden. HF
gepostet vor 17 Jahre, 1 Monat von azamaroth
Original von Nightflyer
UPDATE `tabelle`SET `myint`=myint-$sub WHERE myint-$sub>0 && `id`=$id
...
Ich glaube das hat mir schon weitergeholfen. Vielen Dank.
Werde die Änderung mal so laufen lassen, ich sage mal bescheid ob das geholfen hat.
gepostet vor 17 Jahre, 1 Monat von TheUndeadable
> Ich glaube das hat mir schon weitergeholfen. Vielen Dank.
Wenn du allerdings für die Abbuchung eine Gegenleistung erzeugst (Beispielsweise eine Einheit oder ein Gebäude kreierst), so erzeugst du damit einen hübschen Spawn-Bug.
Man erzeugt Einheiten ohne das was abgebucht wurde.
Wenn du allerdings für die Abbuchung eine Gegenleistung erzeugst (Beispielsweise eine Einheit oder ein Gebäude kreierst), so erzeugst du damit einen hübschen Spawn-Bug.
Man erzeugt Einheiten ohne das was abgebucht wurde.
gepostet vor 17 Jahre, 1 Monat von Nightflyer
Das aber wiederum nur wenn man die vorangehenden Update-Query nicht mit mysql_affected_rows() geprüft hat
Halten wir fest:
$query = mysql_query("... " //Update Query mit Prüfung
if( mysql_affected_rows( $query ) == 0 ){
//Kein Abzug von Ressourcen/Gebäude/Einheiten erfolgt
return false;
}
//Weiter weil erfolgreich
Halten wir fest:
$query = mysql_query("... " //Update Query mit Prüfung
if( mysql_affected_rows( $query ) == 0 ){
//Kein Abzug von Ressourcen/Gebäude/Einheiten erfolgt
return false;
}
//Weiter weil erfolgreich
gepostet vor 17 Jahre, 1 Monat von knalli
Hä.. hab ich das nicht gestern schon alles geschrieben?