Paralleler Datenbankzugriff

  • Hi!
    Ich bastle gerade an einem PHP-Skript zum auslesen von Spielstatistiken aus einer MySQL-Datenbank. Die Statistiken werden von einem Bot geschrieben, auf dessen Art und Weise die Tabelle zu schreiben ich keinen Einfluss habe. In der Datenbank gibt es eine Tabelle, die mit jedem Spiel 10 Zeilen, die den Statistiken der 10 Spieler aus dem Spiel entsprechen bekommt.
    Nun ist es natürlich so, dass es auch Spieler gibt, die mehrere Spiele spielen, also öfter in der Tabelle auftauchen. Ich möchte nun per PHP eine neue Tabelle erzeugen, die jeden Spieler nur einmal und in der entsprechenden Zeile die gesammten Statistiken des Spielers enthält.
    Mit jedem aufruf des Skripts, dass die zweite Tabelle in irgendeiner Art und Weise präsentiert, soll diese auch gleichzeitig aktualisiert werden. Da dies aber bei einer großen Spielzahl eher länger dauert und und ich das nicht bei jedem Seitenaufruf haben möchte, lese ich aus der ersten Tabelle nur bisher ungelesene Einträge aus, indem ich eine weitere Spalte "read" hinzufüge und diese auf nach dem lesen auf 1 setze. Die gelesenen Statistiken werden dann in die zweite Tabelle integriert.
    Und jetzt kommt das Problem:
    Es ist zwar unwahrscheinlich, aber darf nicht außer Acht gelassen werden, dass zwei gleichzeitig ausgeführte Skripte, die beide die ungelesenen Zeilen aus der ersten Tabelle in die zweite Tabelle integrieren wollen sich in die Quere kommen, das heißt sie lesen beide gleichzeitig und integrieren die Zeilen doppelt, was die zweite Tabelle verfälscht.
    Dass das möglich ist, habe ich mit diesem Skript ausprobiert:

    Jetzt zwei fragen:
    1. Wie kann ich das verhindern?
    2. Kann ich wenigstens davon ausgehen, dass der MySQL-Server zwei Queries, die auch nur ansatzweise miteinander zu tun haben, NACHEINANDER ausführt?

    Vielen Dank fürs Durchlesen! ;)

    Regelmäßiges Versagen ist auch eine Form der Zuverlässigkeit.

  • 1. Wie wäre es mit sleep(5)
    2. Du kannst nicht davon ausgehen, wenn du sie getrennt losschickst.
    Du kannst aber mehrere Queries durch ein Semikolon als Separator zusammenfügen. Alternativ kannst du sem_get, sem_acquire und sem_release verwenden. Achte aber darauf, dass sem_release immer aufgerufen wird.

  • 1. Hatte ich zuerst, aber dann habe ich gedacht, sleep könnte vielleicht ein Sonderfall sein und habe es vorsichtshalber ersetzt.
    2. Ok danke, ich schau mir die Funktionen mal an.

    Regelmäßiges Versagen ist auch eine Form der Zuverlässigkeit.

  • Super, das klappt!
    Allerdings nicht auf meinem Localhost... ich hab irgendwie gelesen, dass ich da vielleicht noch was aktivieren muss oder so. Der Server ist auch nicht mehr so neu, Wamp5 1.6.4 kann der das überhaupt?

    Und gibt es nicht noch ne möglichkeit das direkt aufm MySQL-Server zu regeln? Für mich reicht das ja so eigentlich erstmal, aber es könnte ja auch von woanders drauf zugegriffen werden, zum beispiel per phpmyadmin, oder wird das da auch geregelt?

  • Okay, also dieses "LOCK TABLES" scheint das zu sein, was ich brauche. Allerdings habe ich bei der Hälfte der Erklärung nur Bahnhof verstanden, weil ich auch in sachen SQL eigentlich keine Ahnung habe... :roll: Ich denke, ich könnte bei dieser Uptdate-Geschichte ein bisschen Hilfe gebrauchen...

    Ich erkläre das Problem noch mal ein bisschen genauer:
    Es geht um einen Bot, der im Warcraft 3 - Battle.net Spiele hostet und am Ende jedes Spiel die Datenbank aktualisiert. Das umfasst im wesentlichen vier Tabellen:

    games - ein neuer Eintrag
    dotagames - ein neuer Eintrag, wenn das Spiel ein Dotaspiel war (in der Regel immer)
    gameplayers - soviele neue Einträge, wie Spieler im Spiel waren
    dotaplayers - 10 neue Einträge, wenn das Spiel ein Dotaspiel war (in der Regel immer)

    Jeder Eintrag in jeder Tabelle hat natürlich eine fortlaufende Id zur eindeutigen Identifizierung. Jede der Tabellen mit Ausnahme der Tabelle "games" besitzt eine Spalte "gameid", in der die Id des zugehörigen Eintrags in der games-Tabelle steht.
    In der Tabelle "dotaplayers" gibt es eine Spalte "colour". Sie beinhaltet die Farbe des Spielers in Form einer Zahl von 1 bis 11 ohne 6. Da nach jedem Spiel 10 Spieler hinzugefügt werden, gibt es für alle Einträge mit der gleichen "gameid" jede Farbe genau einmal.
    In der Tabelle "gameplayers" gibt es diese Spalte "colour" ebenfalls. Hat ein Eintrag aus der Tabelle "gameplayers" die gleiche "gameid" und "colour", wie ein Eintrag aus der Tabelle "dotaplayers", dann handelt es sich um den selben Spieler in dem selben Spiel. Weil allerdings nicht jedes Spiel ein Dotaspiel ist (könnte ja mal passieren) und nicht jedes Dotaspiel mit 10 Spielern gespielt wird (es gibt also Dummyeinträge), gibt es nicht für jeden "gameplayer" einen "dotaplayer" und andersrum ebenfalls.
    Nun ist es außerdem so, dass der Name, der zur Identifikation des Spielers dient, NUR in der "gameplayers"-Tabelle in der Spalte "name" vorhanden ist. Er ist übrigens auch das einzige Feld, das ich von dieser Tabelle brauche, da die übrigen Statistiken für diese "Update-Geschichte" irrelevant sind.
    In der Tabelle "dotagames" sind drei Spalten wichtig. Die erste nennt sich "winner". Ist der Wert eines Eintrags hier 0, so wurde das Spiel nicht beendet und es gibt keine auswertbaren Statistiken. Ist der Wert 1 oder 2, so hat "Sentinel" oder "Scourge" gewonnen. Die Spalten "min" und "sec" beinhalten die Spieldauer in Minuten und Sekunden.

    Um zur Sache zu kommen:

    Ich möchte eine neue Tabelle erstellen, in der für jeden Spieler aus der "dotaplayers"-Tabelle, der mindestens ein Dotaspiel beendet hat (winner != 0), eine Zeile vorhanden ist. In dieser Zeile sollen einige Statistiken aus den Einträgen der beendeten Spiele in der "dotaplayers"-Tabelle summiert stehen. Außerdem sollen dort die Anzahl der Spiele, die Anzahl der Siege und die gesammte Spielzeit des Spielers stehen.

    Beispiel:
    Ein Spieler mit dem Nicknamen hans hat insgesammt 2 Dotapiele beendet, eins mit der Dauer 40:30, das andere mit der Dauer 29:30, 1 Spiel davon hat er gewonnen. Betrachte ich nur die Spalte "kills" in "dotaplayers" (es gibt noch mehr): Im ersten Spiel hat er 10 Kills bekommen, im zweiten 12.
    Der eintrag in der neuen Tabelle sieht also wie folgt aus:

    name: hans
    games: 2
    wins: 1
    playtime: 4200 (in Sekunden)
    kills: 22
    ...

    Außerdem sollen in die Tabellen noch ein paar Spalten eingetragen werden, die sich aus der Tabelle selbst ergeben, nämlich "winratio" (in diesem Fall 0.5), für alle Statistiken wie "kills" noch eine "-ratio" nach der Formel "kills / playtime * avaragegametime", wobei avaragegametime die durchschnittliche Dauer eines Dotaspiels ist, und noch eine Spalte, die ich "strength" getauft habe und für die ich mir eine komplexere Formel überlegt habe, die von "winratio", "games" und allen ratio-Statistiken wie "killratio" abhängt.

    So weit, so gut, aber wie realisiere ich das?
    Jedes mal, wen sich jemand eine Seite anschaut, die auf dieser Tabelle aufbaut, soll ein Query aufgerufen werden, der die Tabelle aktualisiert und nach einer gewissen Sortierung ausliest. Damit nicht jedesmal jedes Spiel und jeder Spieler ausgelesen wird, möchte ich die bereits gelesenen Spiele markieren, indem ich in der Tabelle "dotagames" eine neue Spalte "read" erstelle, die wahr oder falsch sein kann.
    Im diesem Query müssen zunächsteinmal die beiden Tabellen irgendwie verriegelt werden (irgendwie LOCK TABLES), damit doppelte Aufrufe des Skripts sich nicht in die Quere kommen. Dann werden aus allen vier Tabellen passende 4-Tupel an Einträgen ausgewählt:

    SQL
    SELECT * FROM ´dotaplayers´, ´gameplayers´, ´dotagames´, ´games´ WHERE ´games´.´id´ = ´dotagames´.´gameid´ AND ´games´.´id´ = ´gameplayers´.´gameid´ AND ´games´.´id´ = ´dotaplayers´.´gameid´ AND ´dotagames´.´winner´ != 0 AND ´dotagames´.´read´ = FALSE

    Habe ich das soweit richtig? Vorallem bei dem "!=" und dem "FALSE" bin ich mir nicht wirklich sicher...

    Jetzt muss ich alle obengenannten Daten unter dem entsprechenden Eintrag (Schlüssel "name") in der neuen Tabelle aktualiseren und ab jetzt habe ich keine Ahnung mehr, denke ich. Was ist zum Beispiel, wenn der Eintrag noch nicht existiert, und wie addiere ich etwas zu einem Feld wie "kills".
    Für die zusätzlichen Spalten wie "killratio" brauche ich auch noch die durchschnittliche Spieldauer, die ich aus allen Einträgen der "dotagames"-Tabelle errechnen könnte, aber wie Speichere ich mir soetwas?

    Abschließend muss ich dann noch alle Werte der Spalte "read" der gelesenen Einträge auf true setzen und die Tabellen entriegeln.

    In der Hoffnung, dass irgendjemand, der Ahnung von MySQL hat, bis zum Ende gelesen hat und sich für mein Problem interessiert

    Speedy Consoles

    Regelmäßiges Versagen ist auch eine Form der Zuverlässigkeit.

    Einmal editiert, zuletzt von Speedy Consoles (15. Dezember 2009 um 23:14)

  • kannst du uns mal ein SHOW CREATE TABLE `x` von deinen Tabellen machen, weil es hört sich so an als hätte die die falsche Struktur, vielleicht kann man die ja noch optimieren und vielleicht würde dann auch das LOCK TABLE überflüssig. Den Teil mit der Spalte `read` verstehe ich nicht, wenn du das so meinst, dass es für den User als gelesen makiert werden soll, dann müsstest du das individuell speichern, weil das so sonst für alle gelten würde.

    Gruß crAzywuLf :D

  • Die Spalte "read" soll dafür da sein, damit ich nicht jedes mal die neue Tabelle neuschreibe, sondern nur um die neuen Spiele erweitere.

    Hier die Tabellen:
    games:

    dotagames:

    Code
    CREATE TABLE `dotagames` (
      `id` int(11) NOT NULL auto_increment,
      `botid` int(11) NOT NULL,
      `gameid` int(11) NOT NULL,
      `winner` int(11) NOT NULL,
      `min` int(11) NOT NULL,
      `sec` int(11) NOT NULL,
      PRIMARY KEY  (`id`)
      ) ENGINE=InnoDB DEFAULT CHARSET=latin1

    gameplayers:

    dotaplayers:

    Regelmäßiges Versagen ist auch eine Form der Zuverlässigkeit.

  • hmm... ich habe immo zuviel um die Ohren, du könntest ja mal noch den Teil deines PHP Scripts zeigen in dem die Überschneidung stattfinden würde, aber probier es erstmal selbst mit LOCK TABLES.
    Das was du verhindern willst musst du sperren:

    PHP
    <?php
    mysql_query("LOCK TABLES test WRITE");
    mysql_query("UPDATE test SET a = 1 WHERE b = 1");
    mysql_query("UPDATE test SET a = 2 WHERE b = 2");
    mysql_query("UPDATE test SET a = 3 WHERE b = 3");
    mysql_query("UNLOCK TABLES");
    ?>

    bei dieser Variante verhinderst du mit LOCK TABLES, dass während deines UPDATE Statements keine anderen Schreibzugriffe auf diese Tabelle zugelassen werden. Du musst halt überlegen was kritisch wäre, während dein Script durchläuft und die Schreib- und Leserechte für die entsprechenden Tabellen sperren.
    Ich finde es eigentlich nicht so schlecht erklärt in dem Referenzhandbuch, im Endeffekt geht Probieren über studieren, also testen testen testen.

    Gruß crAzywuLf :D

  • Bis eben gerade gab's noch gar kein Skript...
    Ich habe jetzt angefangen die Sache mit einer Mischung aus SQL und PHP zu lösen:

    Klappt auch soweit. Allerdings konnte ich, als ich das LIMIT noch nicht eingestellt hatte (ist jetzt zu Testzwecken so, sonst gibts n Fatal Error wegen zu langer Skriptausführung) die Tabelle "playerstats" per phpmyadmin einfach mal so leeren... Soviel zum Thema LOCK TABLES :roll:. Warum?

    Regelmäßiges Versagen ist auch eine Form der Zuverlässigkeit.

    4 Mal editiert, zuletzt von Speedy Consoles (20. Dezember 2009 um 17:02)

  • *Skript uptdated*

    Funktioniert soweit, die Tabelle stimmt, allerdings klappt das mit dem Tabellensperren wohl nicht und es dauert mir insgesamt ein bisschen zu lange...

    EDIT:

    *Skript uptdated*
    Habe eine neue Tabelle hinzugefügt, die statt des spielenden Spielers den gespielten Helden als key hat, sonst alles gleich...

    Regelmäßiges Versagen ist auch eine Form der Zuverlässigkeit.

    2 Mal editiert, zuletzt von Speedy Consoles (20. Dezember 2009 um 17:01)