mmofacts.com

MySQL Probleme...

gepostet vor 13 Jahre, 6 Monate von -Iceman-

Hallo,

ich probiere mich momentan ein bisschen an einem Kampfscript. Das Problem handelt sich allerdings nicht primär um den Kampf, sondern eher um die Datenverwaltung.

Problem: 20 User sind im Kampf angemeldet. Jeder User trifft auf einen anderen. Es müssen ziemlich viel Berechnungen erledigt werden (Waffen, Erfahrung, Skillung etc.). Sobald ein User auf einen anderen getroffen ist, werden diese beide aus der Tabelle für den Kampf in der Datenbank gelöscht. Bis hierher alles wunderbar. Sollte es nur noch ein User geben und keinen Gegner mehr, so bekommt der eine Entschädigung und eine entsprechende Nachricht zugesendet. Nun passiert es aber AB UND ZU (ich vermute das könnte an der Serverauslastung liegen) das der User zweimal diese Nachricht bekommt inklusive der Entschädigung (auch doppelte Erfahrungspunkte etc.), das heißt der Scriptteil wird doppelt ausgeführt. Alle Variablen etc. sind 100% richtig gesetzt, da ich es auf meinen lokalen Server schon hundert mal laufen lasse hab... Dieses "Phänomän" habe ich auch schon in anderen Scripten gehabt, in welchem ziemlich viel Funktionen ausgeführt werden. Ich schließe einen direkten Scriptfehler aus, ich vermute das es am MySQL liegt. Mit Funktionen wie Begin und Commit hab ich mich auch schon beschäftigt, bin mir aber nicht ganz sicher ob dies Abhilfe schaffen würde bzw. ob es nicht alternativen gibt. Ich habe auch auf InnoDB umgestellt.

Also die eigentlichen Fragen lauten eigentlich: kennt ihr dieses Problem? Woran könnte dies liegen? Habt ihr einen Lösungsvorschlag oder Hinweis?

gepostet vor 13 Jahre, 6 Monate von Klaus

Klingt doch sehr nach Race condition. Wie wird denn bei dir der Kampf gestartet?

gepostet vor 13 Jahre, 6 Monate von NeoArmageddon
Halte das "zwischenspeichern" von Daten in der Datenbank eh für eine "unelegante" Variante, denn genau hier treten eigentlich bei fast allen zeitkritischen Aktionen solche Probleme auf. Ich weiß nicht in welcher Sprache dein Skript ist, aber vermutlich wird es möglich sein, deine Spieler die am Kampf teilnehmen in einen mehrdimensionalen Array packen. Man kann dann auch schön mit nem count raus finden, ob nur noch 1 Spieler dabei ist und das Problem mit deiner Race Condition (also das ein Skript zweimal auslöst, weil das Löschen aus der Datenbank langsamer ist als ein 2er Tick deines Skriptes) löst sich auch.
gepostet vor 13 Jahre, 6 Monate von splasch

Ich würde auch auf Race condition Tipen. Bassierd das bei dir Lokal auch oder nur auf dem Online Web Server.

Spieler provozieren das gerne um sich Spielvorteile  zuverschaffen durch schnelles (F5) Aktualisieren. Entgegen wirken kannst du dem wenn du Transaktionen verwendest.

gepostet vor 13 Jahre, 6 Monate von -Iceman-

Ich versuche mal etwas mehr von meiner Datenbankstruktur zu erläutern, da ich auf ein paar weitere Tipps hoffe. Das mit den mehrdimensionalen Arrays werde ich aber auf jedenfall probieren größtenteils umzusetzen.
Also User A geht arbeiten. Dies dauert 30 Minuten. Entsprechend gibts einen DB eintrag in die Tabelle "aktionen". Nach 30 Minuten wird dann die Aktion beendet und das entsprechende Ereignis ist da. Dann gibt es den o.g. Kampf, dieser findet täglich um 10Uhr statt und jeder User kann sich vorher anmelden. Sobald der Kampf startet, gibts wieder einen DB Eintrag in die Tabelle "aktionen". Ist der Kampf beendet, findet der Kampf für das Script offiziell statt (da die Berechnung ja nichtmal eine sekunde dauert) und wird ausgeführt.
Scriptaufbau ist im groben bisher so:
User A wird ausgelesen aus der Tabelle "aktionen" in einer while schleife, alle wichtigen Berechnungen durchgeführt und die Kampfkraft berechnet, dies geschieht dann auch mit User B auf den User A trifft. Kampf wird beendet - DELETE in der Tabelle "aktionen" für beide User. Das ganze wiederholt sich dann in einer Schleife, bis alle User "fertig" sind.

Lokal ist mir der Fehler bisher nicht aufgefallen. Programmiert wird übrigens in PHP.

Ich bedanke mich bisher schonmal bei allen Tipps und Hilfen :)

gepostet vor 13 Jahre, 6 Monate von Nightflyer

Wird ne Race COndition sein. Quick n' Dirty kannst du einfach mal versuchen ein sleep(2); in deine Schleife einzubauen. Das hilft je nachdem auch schon.

gepostet vor 13 Jahre, 6 Monate von Phoenix1988

Also den sleep würde ich nicht empfehlen, das ist schon sehr schmutzig ;)

Soweit ich es verstanden habe ist das Ganze wie ein Turnier aufgebaut. In der Datenbank speicherst du also eigentlich nur die Teilnehmerliste. Die würde ich dann möglichst in einem Query laden und wie schon vorgeschlagen in einem mehrdimensionalen Array speichern. Wenn du den Aufbau der relevanten Tabellen postest könnte man schauen wie viele Querys du brauchst und wie die aussehen müssten. Oft kannst du mit einem Query gleich alle User kriegen und zumindest Teile der Berechnung auch im Query erledigen, was dir noch Performance bringt da die DB dort sehr schnell ist.

Der Rest des Turniers würde dann nur im Skript ablaufen, erst für das Ergebnis hast du zwei Querys: Lösche alle Kämpfe und speichere die Entschädigung.

Was ich als Faustregel kenne: DB-Abfragen in Schleifen sollte man vermeiden.

Mit Transaktionen zu arbeiten würde ich dir aber zusätzlich empfehlen.Es könnte dein Problem hier auch lösen, aber mit der anderen Lösung bist du meiner Meinung nach deutlich performanter unterwegs. 

gepostet vor 13 Jahre, 6 Monate von Sarge

Original von Nightflyer

Wird ne Race COndition sein. Quick n' Dirty kannst du einfach mal versuchen ein sleep(2); in deine Schleife einzubauen. Das hilft je nachdem auch schon.

setzen, 6 und aberkennung des Entwicklerstatus bitte oO.

Wie dir jeder hier schon gesagt hat, ist es vermutlich eine Race-Condition - zwei Prozesse versuchen die gleiche Aktion zu berechnen. Da du - wenn man zwischen den Zeilen liest - eigtl. ein MyIsam Benutzer bist, wäre die einfache Lösung eine neue Spalte einzuführen locked, default 0 und am anfang deines Scriptes eine überprüfung, ob der prozess tatsächlich die selektierte action ausführen darf ala: query("update action_table set locked=1 where id=$id and locked=0"); if(mysql_affected_rows() != 1) return;

Allerdings bewahrt dich das nur davor, dass zwei Prozesse die gleiche Aktion berechnen. Du wirst nun immernoch in Race-Conditions kommen wenn zwei Prozesse zwei unterschiedliche Aktionen berechnen die die gleichen Ressourcen (z.B. den gleichen Spieler) betreffen. Dann überschreibt die eine Aktion das Ergebnis der anderen Aktion o.ä. . D.h. du musst dich z.B. auch um das Locken der Resourcen kümmern. Achtung bei mehreren Ressourcen besteht natürlich die Gefahr von sog. Deadlocks.

Alternative Du steigst tatsächlich auf InnoDB um und nutzt Transaktionen. Da hilft es aber nicht einfach die Engine um zu stellen, den default mäßig ist jede Query eine eigenständige Transaktion (Stichwort auto_commit). Du musst explizit die kritischen zusammengehörigen Teile identifizieren und in gekapselte Transaktionen (dieses komische begin() und comit() und im fehlerfall rollback() )  packen. Dabei darauf achten das die Transaktion nicht zu groß werden und sie möglichst schnell Fehlschlagen.

Btw. wenn du Dich nun ausgiebig mit den RaceConditions in deinem Spiel beschäftigt hast und eine Lösung ausgedacht hast, ist es in der Regel ein guter Zeitpunkt, dein Spiel einmal komplett neu zu schreiben und das bis dahin gelernte anzuwenden (ohne dich jetzt demotivieren zu wollen).

gepostet vor 13 Jahre, 6 Monate von NeoArmageddon
In meinem Spiel ist der Kampf fast 1:1 zu deinem. Jedoch lasse ich dort die normalen Aktionen nicht über den Kampf laufen. Dort sind die Tunieranmeldungen extra (Und wenn User fürs Tunier gemeldet ist, dann kann er keine Aktion ausführen). Sobald das Turnier startet, wird aus der Anmeldeliste eine Array erstellt mit alles Usern und Fähigkeiten (JOIN ftw). Anschließend läuft eine Schleife im Skript und führt für jeden Kampf meine Kampffunktion/Klasse aus. Wenn ein User rausfliegt wird seine ID/Name zusammen mit Platz und Gewinn in einen 2. Array gepackt und aus dem ersten gelöscht. Wenn der erste Array nurnoch 1 Eintrag hat, ist der Kampf vorbei. Dann kümmert sich der Skript um die Gewinnverteilung anhand des 2. Arrays, leert die Turnieranmeldeliste und schreibt in einer 2. Tabelle die Ergebnisse. So hast du eine geringe Datenbankauslastung und kaum eine Möglichkeit eine Race Condition zu schaffen. P.S. Wenn dein Kampf wirklich Turnierähnlich ist, baue einen Sonderfall ein, wenn die Spieleranzahl ungerade ist, ich hatte das damals zunächst in den Tests vergessen und das gab später die lustigsten Ergebnisse.
gepostet vor 13 Jahre, 6 Monate von splasch

Original von -Iceman-

Ich versuche mal etwas mehr von meiner Datenbankstruktur zu erläutern, da ich auf ein paar weitere Tipps hoffe. Das mit den mehrdimensionalen Arrays werde ich aber auf jedenfall probieren größtenteils umzusetzen.
Also User A geht arbeiten. Dies dauert 30 Minuten. Entsprechend gibts einen DB eintrag in die Tabelle "aktionen". Nach 30 Minuten wird dann die Aktion beendet und das entsprechende Ereignis ist da. Dann gibt es den o.g. Kampf, dieser findet täglich um 10Uhr statt und jeder User kann sich vorher anmelden. Sobald der Kampf startet, gibts wieder einen DB Eintrag in die Tabelle "aktionen". Ist der Kampf beendet, findet der Kampf für das Script offiziell statt (da die Berechnung ja nichtmal eine sekunde dauert) und wird ausgeführt.
Scriptaufbau ist im groben bisher so:
User A wird ausgelesen aus der Tabelle "aktionen" in einer while schleife, alle wichtigen Berechnungen durchgeführt und die Kampfkraft berechnet, dies geschieht dann auch mit User B auf den User A trifft. Kampf wird beendet - DELETE in der Tabelle "aktionen" für beide User. Das ganze wiederholt sich dann in einer Schleife, bis alle User "fertig" sind.

Lokal ist mir der Fehler bisher nicht aufgefallen. Programmiert wird übrigens in PHP.

Ich bedanke mich bisher schonmal bei allen Tipps und Hilfen :)

Umständlicher gehts wohl kaum noch wozu liest du immer alles einzeln aus. Völlig unötig und performenc lastig. In Schleifen mehre Sql anweisungen auszuführen ist ein absolutes no go. Belastet den Server unötig und kann bei vielen durchlaufen sogar einen Timeout bewirken. Hast du das mit der Script ausführung unter einer Sekunde nachgeprüft und gemessen?

Warum schreibst du beim start des Kampfes schon wieder was in die Db? (Unötig) Du brauchst ledig einen lese zugriff auf die Db in dem du alle Kämpfer ausliest und nicht jeden einzeln. Dann führt den Kampf aus. Danach schreibst du das Ergebniss zurück in die Db. Führst ein update für den Status aus das nun der Kampf bendet ist.

Noch einfacher ist es wenn du mit Objekten arbeiten würdest da hast du dann für jeden Kämpfer ein eigenes Objekt mit allen eigenschaften.

Und wie schon erwähnt um sicher zu gehen verwende Transaktionen.

Mfg.

gepostet vor 13 Jahre, 6 Monate von TheUndeadable

Original von Sarge

Original von Nightflyer

Wird ne Race COndition sein. Quick n' Dirty kannst du einfach mal versuchen ein sleep(2); in deine Schleife einzubauen. Das hilft je nachdem auch schon.

setzen, 6 und aberkennung des Entwicklerstatus bitte oO.

Er wollte damit eine Race-Condition zur Verifikation provozieren.

gepostet vor 13 Jahre, 6 Monate von D4rk5in

Ich hatte das selbe Problem. Race Conditions treten immer dann auf, wenn in der Datenbank zeitgleich zwei oder mehr Clienten auf eine einzelne Ressource zugreifen. Du kannst aber den Kampf zu einer logischen Einheit zusammenfassen, die sich dann Transaktion nennt. Dann werden nämlich alle Statements zeitgleich oder gar nicht ausgeführt. Zusätzlich solltest du mal nach Begriffen wie "LOCK IN SHARE MODE" oder "SELECT ... FOR UPDATE" suchen. Damit kannst zur Not nämlich auch einzelne Spalten sperren.

MfG

Auf diese Diskussion antworten