• Es freut uns dass du in unser Minecraft Forum gefunden hast. Hier kannst du mit über 130.000 Minecraft Fans über Minecraft diskutieren, Fragen stellen und anderen helfen. In diesem Minecraft Forum kannst du auch nach Teammitgliedern, Administratoren, Moderatoren , Supporter oder Sponsoren suchen. Gerne kannst du im Offtopic Bereich unseres Minecraft Forums auch über nicht Minecraft spezifische Themen reden. Wir hoffen dir gefällt es in unserem Minecraft Forum!

MySQL und Threads und readLock

CubBossa

Schafhirte
Registriert
17 Juli 2015
Beiträge
125
Diamanten
369
Minecraft
CubBossa
Hallo da draußen :D
Ich bin kein Programmierer sondern 3D-Entwickler und in der Programmierung daher eher "oberflächlich" unterwegs sag ich mal. Für paar Ergänzungen auf meinem Server reicht es denke ich auf jedenfall aus, aber eine Sache macht mich zunehmend nervös und weil ich ja weiß, dass hier viele Programmierer ihr Unwesen treiben, wollte ich mal bisschen euren Rat erfragen.
Und zwar mache ich meine MySQL abfragen ganz normal in gewöhnlichen Methoden ohne irgendwelchen readlocks oder synchronizeds, geschweige denn irgendwelchen ausgelagerten Threads, wenn man das denn nun so macht :shrug:

Also hier ein Beispiel meiner einen MySQL Methode, die ich ganz normal im restlichen Code aufrufe. (Also ich habe eine ArrayList die die Generatoren runtime speichert und speichere dann regelmäßig ab, mit einem Scheduler. Und wenn der Spieler den Server verlässt, zb)

Java:
    public boolean saveGenerator(GeneratorObject g) {
        Connection con = connect();
        boolean saving = false;
        if(con == null) return saving;
        if(!g.isPlaced()) return saving;
      
        try {
            String statement = "UPDATE " + TABLE_NAME + " SET level = ?, isPlaced = ?, world = ?, x = ?, y = ?, z = ? WHERE (uuid = ?) AND (id = ?)";
            PreparedStatement ps = con.prepareStatement(statement);
            ps.setInt(1, g.getLevel());
            ps.setInt(2, g.isPlaced() ? 1 : 0);
            ps.setString(3, g.getFurnace().getLocation().getWorld().getName());
            ps.setInt(4, g.getFurnace().getLocation().getBlockX());
            ps.setInt(5, g.getFurnace().getLocation().getBlockY());
            ps.setInt(6, g.getFurnace().getLocation().getBlockZ());             
            ps.setString(7, g.getOwnerUUID().toString());
            ps.setInt(8, g.getId());
          
            ps.executeUpdate();
            ps.close();
            saving = true;
        } catch (Exception ex) {
            ex.printStackTrace();
            return false;
        }
        try {
            con.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return saving;
    }

Meine Frage ist: Wäre es angebracht, hier irgendwie anders vorzugehen? Wenn ich in den Code von anderen Entwicklern schaue, finde ich oft locks oder threads oder so, sobalds um datenspeichern geht.

Da ich mir das ganze Programmieren nebenher beibringe und durch googlen hier jetzt eben nicht so recht schlau wurde, bzw nur Denkanstöße bekommen habe, würde ich mich über eure Tipp jeder Art freuen :) gern auch, wenn ich ganz andere gravierende Fehler in dem Code gemacht habe.
Lg, CubBossa

Edit: Dass das isPlaced() abspeichern sinnlos ist, ist mir grad schon aufgefallen :D Überhaupt, dass sie in der MySQL ist. Hab während dem coden umgedacht und es anders gelöst und da ist noch ein überbleibsel
 
Zuletzt bearbeitet:

Chrisliebär❤️

nur echt mit ❤️
Moderator
Registriert
19 Mai 2014
Beiträge
1.675
Diamanten
830
Deine Vermutung ist korrekt. Jede Anfrage an eine Datenbank ist oft eine IO Operation. Die Bedingungen, unter denen deine Anfrage direkt aus dem RAM der Datenbank beantwortet wird, liegen in der Regel außerhalb deiner Kontrolle. Das heißt du blockierst den Mainthread und erzeugst damit Lags. Das ist auch einer der Hauptgründe, warum ich persönlich komplett davon abrate Datenbanken in Plugins einzusetzen, bis es unbedingt notwendig wird.

Ein sehr ähnliches Problem tritt übrigens auch auf, wenn du mit dem Spielerlogin Daten aus der Datenbank läds, auch hier wird der Main Thread blockiert. Normalerweise würden asynchrone Anwendungen dir erlauben den Login über ein Future abzuschließen, aber da Bukkit das im Hauptthread macht, illustriert das sehr gut, das Problem, dass du in Bukkit (teilweise auch aufgrund der undurchdachten API) hast. Ein guter Trick ist hier z.B. das einkommende Login Paket mit ProtocolLib abzufangen und nach erfolgreichem Laden der Spielerdaten, das Packet neu einzureihen. Das sind aber alles sehr individuelle Lösungen für einzelne Probleme.

Wenn du deine Abfragen allerdings grundsätzlich asynchron ausführst, dann hast du Duplication Bugs und ähnliches, da die Spielwelt weiter simuliert wird, während deine Anfrage läuft, ich denke das ist klar. Daher ist das auch in der Tat alles gar nicht so einfach und du musst dir genau überlegen, wie du deine asynchronen Anfragen an die Datenbank mit dem Main Thread synchronisierst. Die einzige Möglichkeit ist also alles zu verhindern, was ein Spieler tun kann, während du die Datenbank manipulierst. Das sind in der Regel sehr viele Dinge, die man blockieren und verwalten müsste. Daher ist es oft einfacher, so viele Informationen wie möglich im RAM zu halten und nur beim Login aus der Datenbank zu lesen. Aber auch hier besteht eine Gefahr: Beispielsweise können Servercrahs dazu führen, dass nicht alle Daten gespeichert werden. Im schlimmsten Fall werden z.B. nur die Inventare einiger Spieler geschrieben, das ermöglich Duplizierungsbugs und verschwundenen Items, je nach dem, ob zwischen den Inventaren der verschieden Spieler zuvor gehandelt wurde.

Du solltest dir außerdem das hier anschauen, da deine Ressourcenverwaltung nicht ganz in Ordnung ist, das sollte dir aber eigentlich auch deine IDE sagen. https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html

Außerdem ist das ganz ganz böse, dass du scheinbar für deine Abfragen jeweils eine neue Datenbankverbindung aufbaust, das macht dir so ziemlich alles kaputt, was nur möglich ist. Stattdessen solltest du dir überlegen DataSource zu nutzen: http://openbook.rheinwerk-verlag.de/javainsel9/javainsel_24_011.htm (den JNDI Teil überspringen, das erklärt der nächste Link) Das erlaubt dir nämlich auch automatisch die Verbindungen in einen definierten Zustand zu bringen, falls mal irgendwo ein Fehler auftrat. Für MariaDB, geht das recht simpel: https://mariadb.com/kb/en/pool-datasource-implementation/

Was du dann allerdings ebenfalls beachten musst, ist, dass die Datenbank selbst auch in der Lage ist mehrere Queries parallel auszuführen. Hast du also externe Dienste oder asynchrone Events in Bukkit, so sind auch die Queries für Race Conditions anfällig. Selbst dein eigener Code, kann, sofern er durch die DataSource mehrere Verbindungen nutzt, eine Race Condition auslösen. In manchen Fällen braucht man aber auch sogar mehrere Datenbankverbindungen, da man nur ein aktives ResultSet pro Verbindung haben kann.

Wenn du ein bisschen über der Vorhaben erzählst, können wir vielleicht ein paar Möglichkeiten finden das Problem so zu vereinfachen, dass es einfacher zu verwalten wird.
 
Zuletzt bearbeitet:

CubBossa

Schafhirte
Registriert
17 Juli 2015
Beiträge
125
Diamanten
369
Minecraft
CubBossa
Genau, so hab ich mir das auch alles bisher erklärt in etwa. Jetzt frag ich mich - es gibt ja einen weg, damit umzugehen. Würde man also die Datenbankzugriffe asynchron machen und ein lock Objekt verwenden, um diese duplication bugs zu verhindern?
Und wie sähe das denn in etwa in einem Plugin Code aus? Benutzt man dafür irgendwelche formen von getScheduler() und wie kann ich mir das vorstellen?

Du sagst Datenbanken vermeiden, kann ich verstehen ^^ aber lässt sich in unserem Fall nicht ganz vermeiden, weil wir halt mittlerweile 4 Server haben und sowas wie die Votegems ja auf jedem Server gleich bleiben müssen.
Danke schonmal für deine Einschätzung und das mit dem tryResourceClose schau ich mir auch mal in Ruhe an.

Edit: grad gesehen dass du deine antwort nochmal erweitert hast, lese das dann nochmal in ruhe
=============================
Aaaalso, hab mir alles noch ein paar mal durchgelesen und denke, dass sich, wie du vorgeschlagen hast, die meisten Fragen vllt konkret an einem Beispiel beantworten lassen könnten.

Ich würde dafür mal nicht die Generatoren von oben nehmen sondern unsere Gems. Wir haben einen kleinen Bungeecord Server mit einem Creative, einem Skyblock und einer Lobby. Wenn man votet, bekommt man einen Gem (virtuelle Währung). Diese Gems finden nun an allen Ecken ihre Verwendung: Scoreboard zum Beispiel, oder wenn man sich im Gem-Shop etwas freischalten möchte. (Mobspawning in Creative zb, so garantieren wir uns, dass Spieler ein bisschen spielen und nicht joinen und mit viel Entities lags machen).

Wenn ich das jetzt alles richtig verstehe gehe ich zum Beispiel wiefolgt vor:
in meiner MySQLManager klasse lege ich eine Klassenvariable für die Connection an und prüfe in jedem zugriff lediglich mit ner methode ob null, und wenn null, dann richte die Verbindung ein.

Jetzt teilt sich der Weg meines erachtens schon:
- ich speichere die Gems im Arbeitsspeicher wie du sagst und speichere sie nur regelmäßig / beim join/quit. Nachteil: ein Admin kann auf einem anderen Unterserver des Bungeecords nicht die aktuellsten Gems des Spielers einsehen, außer man arbeitet mit den messagingchannels und laggs bei Join und Disconnect wenn es im Hauptthread läuft. Und Datenverlust, wie du gesagt hast, wenn der Server crasht. Vorteil: nicht viele zugriffe auf db, also eigentlich ganz nice für alleinstehende Server.

- Ich benutze weiterhin überall mysql abfragen, verwende aber eigene threads und benutze ein lock, um zu verhindern, dass eine Race Situation entsteht. Wenn ein Spieler joint wird der Wert im Scoreboard dann zum Beispiel einfach später gesetzt, sobald er aus der MySQL gelesen wurde und der Hauptthread wird dabei nicht beeinflusst. Wenn der Spieler den Gemshop öffnet, öffnet dieser sich, sobald die aktuelle Gemzahl gelesen wurde und der Hauptthread wird nicht beeinflusst und so weiter.

Hab ich das jetzt richtig aufgefasst? Hoffentlich :D
Dann wäre ja in diesem Fall die zweite Lösung sehr viel Sinnvoller.
Hier würde jetzt das DataSource vermutlich auch zum Einsatz kommen, das du erwähnt hast. Ich hatte keine Zeit, das ausführlich zu lesen, werde mich aber dann selbstständig schlau machen :) aber wenn ich das auf die Schnelle richtig verstanden habe, geht es dabei darum, die Anfragen mit nem Pool zu managen/bündeln (?)

Okay und zuletzt zum try-catch. Da habe ich jetzt nochmal verschiedene Seiten gelesen und auch die, die du mir geschickt hast. Aber was ich genau falsch gemacht habe, versteh ich nicht :D

Vielen Dank für deine Hilfe!
 
Zuletzt bearbeitet:

Chrisliebär❤️

nur echt mit ❤️
Moderator
Registriert
19 Mai 2014
Beiträge
1.675
Diamanten
830
Ich würde dafür mal nicht die Generatoren von oben nehmen sondern unsere Gems. Wir haben einen kleinen Bungeecord Server mit einem Creative, einem Skyblock und einer Lobby. Wenn man votet, bekommt man einen Gem (virtuelle Währung). Diese Gems finden nun an allen Ecken ihre Verwendung: Scoreboard zum Beispiel, oder wenn man sich im Gem-Shop etwas freischalten möchte. (Mobspawning in Creative zb, so garantieren wir uns, dass Spieler ein bisschen spielen und nicht joinen und mit viel Entities lags machen).
Okay, das klingt nach einem sehr einfachen Fall. Das klingt bisher, als währe die Währung rein virtuell. Das heißt der Spieler besitzt keine Items oder ähnliches, welche die Währung repräsentieren, es ist einfach nur ein Wert, der irgendwo gespeichert und abgerufen wird. Das ist tatsächlich ein sehr einfacher Fall. Wenn das nicht stimmen sollte, dann gilt der Rest des Beitrages vermutlich nicht mehr.


Wenn ich das jetzt alles richtig verstehe gehe ich zum Beispiel wiefolgt vor:
in meiner MySQLManager klasse lege ich eine Klassenvariable für die Connection an und prüfe in jedem zugriff lediglich mit ner methode ob null, und wenn null, dann richte die Verbindung ein.
Nein, du nutzt, wenn du es absolut richtig machen willst, die DataSource. (weiter unten).

- ich speichere die Gems im Arbeitsspeicher wie du sagst und speichere sie nur regelmäßig / beim join/quit. Nachteil: ein Admin kann auf einem anderen Unterserver des Bungeecords nicht die aktuellsten Gems des Spielers einsehen, außer man arbeitet mit den messagingchannels und laggs bei Join und Disconnect wenn es im Hauptthread läuft. Und Datenverlust, wie du gesagt hast, wenn der Server crasht. Vorteil: nicht viele zugriffe auf db, also eigentlich ganz nice für alleinstehende Server.
Das ist zumindest eine Möglichkeit das Problem zu lösen. Die Nachteile, die du erwähnst, sind jedoch so gravierend, dass ich diese Lösung nicht in Betracht ziehen würde. Die Datenbank hat mit ein paar hundert Anfragen pro Minute überhaupt gar kein Problem.

- Ich benutze weiterhin überall mysql abfragen, verwende aber eigene threads und benutze ein lock, um zu verhindern, dass eine Race Situation entsteht. Wenn ein Spieler joint wird der Wert im Scoreboard dann zum Beispiel einfach später gesetzt, sobald er aus der MySQL gelesen wurde und der Hauptthread wird dabei nicht beeinflusst. Wenn der Spieler den Gemshop öffnet, öffnet dieser sich, sobald die aktuelle Gemzahl gelesen wurde und der Hauptthread wird nicht beeinflusst und so weiter.
Das ist schon theoretisch korrekt, aber "sobald die aktuelle Gemzahl gelesen wurde" ist halt leichter gesagt als getan. Die Fragen, die du dir z.B. stellen musst ist:

  • Was wenn der Spieler versucht mehrere Inventare zu öffnen, während du gerade den Stand abfragst?
  • Was wenn der Spieler offline geht?
  • Sich ausloggt?
  • Ganz viele andere komische Dinge tut

Das lässt sich in der Praxis dann nie alles korrekt absperren. Daher macht es oft Sinn, dass man einfach mit bestimmten Fehlern lebt und primär die Datenintegrität berücksichtigt. Wir ignorieren erstmal die Frage, wie sich das Inventar öffnet und gehen davon aus, der Spieler kauft sich etwas: In diesem Fall, startest du einen Thread im Hintergrund, welcher die Transaktion abwickeln wird. Nun brauchst du eigentlich eine atomare Compare And Set Query, welche zuerst prüft, ob der Spieler wirklich genug Gems hat und dann die Gems abzieht. Das Problem: Das sind zwei Operationen und sie sind nicht atomar. Da könntest du jetzt datenbankspezifische Locks verwenden, aber das führt für einen Anfänger zu weit. Daher ein anderer Vorschlag:

EDIT: Zur Frage, wie du nun den Gemstand bekommst, wenn sich das Invetar öffnet. Die lädst du halt einfach einmal vorher. Wenn der Spieler mehrfach klickt, dann öffnen sich halt mehrere verbuggte Menüs. Wenn du willst, kannst du den Spieler auch einfach blockieren, bis du die Daten hast oder einen generellen Cooldown von 5 Sekunden oder so verwenden. Es kann ja, in Kombination mit dem Rest, nur die grafische Darstellung kaputt gehen. Die Datenintegrität bleibt trotzdem sichergestellt.

Der Spieler kann nur auf einem Server gleichzeitig sein (das musst du sicherstellen, ich nehm das als gegeben an). Jeder Server kann nur genau EINE Transaktion gleichzeitig ausführen. Kaufen also 5 Spieler auf einem Server etwas, so wird das parallel bearbeitet. Daraus kannst du jetzt ableiten, dass es unmöglich ist, dass ein Spieler zwei Transaktionen gleichzeitig parallel am Laufen hat. Gut, also Problem gelöst. Gleichzeitig merkst du, dass du eigentlich nur einen Thread brauchst. Denn mehr als eine Transaktion gleichzeitig ist eh nicht möglich. Hierfür gibt es z.B. Worker Pools, die nur konstant einen Thread haben.

Dieser Thread prüft nun das Guthaben in der Datenbank, ändert es, und erzeugt dann einen Task im Mainthread, welcher die Transaktion abschließt und dem Spieler das Item gibt. Kleiner Anmerkung auch hier: Wenn zwischen dem Abziehen und dem Ausgeben des Items der Server crasht, verliert der Spieler die Gems und bekommt kein Item. Das zu verhindern, würde aber richtig richtig kompliziert werden. Aber solche Überlegungen sollte man sich immer machen und dann entscheiden, ob man diesen Fehler akzeptieren kann.

Damit ist das ganze relativ aufgeräumt. Du brauchst keine Locks und du hast aufgrund deiner Einschränkungen auch keine Probleme mit Race Conditions.

Wie genau der Spieler seine Gems bekommt, haben wir uns nun allerdings nicht angeschaut. Es ist auch hier möglich, dass der Spieler gerade ein Minigame gewonnen hat und einen Einkauf tätigt. Dann würde folgendes passieren (beispielhafte Zahlen zum Verständnis)

  1. Spieler hat aktuell 10 Gems
  2. Spieler öffnet Shop
  3. Spieler möchte Brecheisen für 5 Gems kaufen
  4. Thread liest aktuelles Guthaben mit 10 Gems aus Datenbank und bestätigt, dass 10 > 5
  5. Spieler gewinnt Minigame und bekommt 3 Gems, also 10 + 3 Gems. Datenbank schreibt 13 Gems
  6. Transaktionsthread berechnet 10 - 5 (weil ja 10 gelesen wurden) und schreibt 5 in die Datenbank
Der Spieler hat nun also seine 3 Gems verloren. Um das zu verhindern, muss also auch sichergestellt werden, dass der inkrement atomar ist. Also z.B. eine Query mit "UPDATE ... SET gems = gems +3" benutzt werden. Ein anderer Trick besteht darin die Updatequery grundsätzlich mit dem alten Zustand zu schreiben, also "UPDATE ... SET gems = gems - 5 WHERE gems = $erwarteterWert AND user = "luser"". Wenn du dann die Anzahl der betroffenen Zeilen prüfst, so hast du entweder 1, wenn es keinen Schreibkonflikt gab, oder 0, wenn zwischenzeitlich der Wert geändert wurde, in letzterem Fall wiederholst du die Query, bist du Erfolg hattest.

Das Konzept mit atomaren Zustandsänderungen ist allerdings auch im Prinzip die Definition von threadsicherer Programmierung, von daher ist das nicht so einfach. Wenn du allerdings letzteren Trick verwendest, dann solltest du bemerken, dass du sogar beliebig viele Transaktionen gleichzeitig durchführen können solltest. Die Beschränkung, die ich dir eingangs empfohlen habe, wäre also nicht länger nötig.

Spannend wird es dann nochmal, wenn die Spieler Gems handeln können, denn in diesem Fall musst du zwei Zeilen in deiner Tabelle atomar Updaten. Wenn du soweit kommst, solltest du dich mit "Transactions" im Kontext von Datenbanken beschäftigen, diese erlauben dir Änderungen entweder als Ganzes, oder gar nicht durchzuführen. Das ist aber auch nochmal ein kompliziertes (und spannendes Thema).

Hier würde jetzt das DataSource vermutlich auch zum Einsatz kommen, das du erwähnt hast. Ich hatte keine Zeit, das ausführlich zu lesen, werde mich aber dann selbstständig schlau machen :) aber wenn ich das auf die Schnelle richtig verstanden habe, geht es dabei darum, die Anfragen mit nem Pool zu managen/bündeln (?)
Ja, man kann mit einer DataSource auch mehrere Verbindungen benutzen und ja, das ist ein Pool. Aber du kannst die DataSource auch so konfigurieren, dass sie nur eine einzige Verbindung enthält. Der Grund warum man das möchte ist der, dass die DataSource Implementierung sich selbstständig darum kümmert, dass deine Datenbankverbindung aktiv ist. Wenn du z.B. eine kaputte Query abschickst und die Connection dann an die DataSource zurück gibst, dann wird die DataSource sich darum kümmern, dass sie die Verbindung wieder in einen sauberen Zustand setzt. Wenn die Verbindung abbricht und du die Verbindung anforderst, dann baut die DataSource die Verbindung selbständig wieder auf oder gibt dir den Fehler. Entsprechend hast du auch das Problem nicht mehr, dass deine Datenbankverbindung null ist, denn das sollte sie nie sein. Du baust die Verbindung einmal am Anfang auf und nutzt sie dann. Nur, dass du das halt die DataSource erledigen lässt. Also das solltest du auf jeden Fall tun. DataSources können noch ein paar Dinge mehr, aber das braucht dich nicht interessieren.

Okay und zuletzt zum try-catch. Da habe ich jetzt nochmal verschiedene Seiten gelesen und auch die, die du mir geschickt hast. Aber was ich genau falsch gemacht habe, versteh ich nicht
Statement ist ein AutoClosable. Das heißt, du musst es schließen. IMMER!

Hier etwas code, der das illustriert.

Psuedocode:
try {
    var statement = createStatement("foobar")
    statement.irgendwasDa***ceptionWirft()
 
//möglichkeit 1: falsch
    // wird nie aufgerufen, statement ressource nicht geschlossen, ressourcen leakt und irgendwann speicher voll
    statement.close()
} catch (Egal dontcare) {
//möglichkeit 2: ohne nullcheck auch falsch, mit nullcheck echt hässlich
    statement.close() // wenn es zu keinem fehler kommt, wird es doppelt aufgerufen, wenn createStatement() bereits einen Fehler erzeugt, hast du eine Nullpointerexception, daher nullcheck notwendig
} finally {

}

Wie du siehst, wird im Fehlerfall das Statement nicht mehr geschlossen. Das willst du nicht. In anderen Fällen, hast du mehrere Ressourcen und weißt nicht welche noch null sind, daher braucht man immer einen Nullcheck. Weil das alles Arbeit ist, gibt es try-with-ressources, damit schreibst du einfach nur noch:

Code:
try (var statement = createStatement("foobar")) { // wenn dieser block verlassen wird, kümmert sich java um den .close() aufruf
    statement.irgendwasDa***ceptionWirft()
} catch (Egal dontcare) {

}

Falls das nicht ganz klar ist, dann einfach als Fausregel: Die willst niemals selbst close() aufrufen. Jedes Objekt, dass du damit schließen musst, backst du in den Kopf eines try-with-ressources Blocks. Dann bist du auf der sicheren Seite. Eine Ausnahme, bei der das nicht geht, wäre deine DataSource, da diese einmal zu Beginn des Plugins erzeugt und erst am Ende geschlossen wird. In diesem Fall darfst du ausnahmsweise close() in der onDisable Methode aufrufen und auf das try-with-ressources in onEnable verzichten.
 
Zuletzt bearbeitet:

CubBossa

Schafhirte
Registriert
17 Juli 2015
Beiträge
125
Diamanten
369
Minecraft
CubBossa
Okay vielen vielen Dank!
Das Try-Catch hab ich verstanden und auch nochmal bisschen öffentliche Plugins angeschaut und findet man das auch in der Regel so. Hab ich dann gleich mal bisschen umgesetzt :D

Gems sind tatsächlich eine virtuelle Währung, was deinen Beitrag sehr aufschlussreich gemacht hat! ^^
Das fängt schon bei Kleinigkeiten an, wie dass ich nicht wusste, dass man in einem MySQL-Statement direkt eine Rechnung einbauen kann, also zum Beispiel 5 aufaddieren. Klingt dann im Nachhinein wohl schockierend aber bisher habe ich die Gems aus der Datenbank gelesen, die Rechnung gemacht und den Wert wieder zurückgesendet. Was nach allem was du gesagt hast, seehr Fehleranfällig sein kann :D

Ich fass mal kurz zusammen, wie ichs verstanden habe:
DataSource für Connection in der MySQL klasse zb im konstruktor aufbauen
dann ein haufen Methoden, die alle (und hier bin ich noch nicht ganz sicher) Beispielsweise mit der Bukkit runTaskAsynchronously Methode den Task eben aus dem MainThread rausnehmen. Wenn das nur eine Schreibaktion wie Create Table ist, dann birgt das ja gar keine Risiken weil der Spieler nicht darauf angewiesen ist, dass die Aktion fertig ist.
Und wenn es jetzt um einen Rückgabe Wert geht, wirds nochmal tricky, weil man ja die Spielerhandlung solange beispielsweise blockieren müsste, bis der Wert da ist.
(Spieler öffnet ShopGUI, asynchroner Task holt Gems, gibt an dass sie gelesen sind, GUI öffnet sich, Hauptthread wurde nicht beeinflusst (?))

Ich konnte auf jedenfall echt was lernen und bin vorallem in den Herangehensweisen bestätigt, dass ich jetzt weiß woran ich mich setzen kann :D und sorry, wenn da bei mir in manchen Sachen echt die Basics fehlen

Edit: hier einfach mal ne überarbeitete methode:
Java:
    private static final String UPDATE_BY_UUID_AND_ID = "UPDATE " + TABLE_NAME + " SET level = ?, world = ?, x = ?, y = ?, z = ? WHERE (uuid = ?) AND (id = ?)";
    
    public void saveGenerator(GeneratorObject g) {
        if(!g.isPlaced()) return;
        Bukkit.getScheduler().runTaskAsynchronously(Generator.getInstance(), new Runnable() {
            @Override
            public void run() {
                try (PreparedStatement ps = connection.prepareStatement(UPDATE_BY_UUID_AND_ID)) {
                    ps.setInt(1, g.getLevel());
                    ps.setString(2, g.getFurnace().getLocation().getWorld().getName());
                    ps.setInt(3, g.getFurnace().getLocation().getBlockX());
                    ps.setInt(4, g.getFurnace().getLocation().getBlockY());
                    ps.setInt(5, g.getFurnace().getLocation().getBlockZ());               
                    ps.setString(6, g.getOwnerUUID().toString());
                    ps.setInt(7, g.getId());
                    
                    ps.executeUpdate();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        });
    }
 
Zuletzt bearbeitet:

DayAndNight

Minecrafter
Registriert
21 September 2020
Beiträge
17
Diamanten
303
Du könntest dir auch ein bisschen Tipparbeit ersparen indem du anonyme Methoden verwendest:

Code:
   public void saveGenerator(GeneratorObject g) {
        if(!g.isPlaced()) return;
        Bukkit.getScheduler().runTaskAsynchronously(Generator.getInstance(), () -> {
      
                try (PreparedStatement ps = connection.prepareStatement(UPDATE_BY_UUID_AND_ID)) {
                    ps.setInt(1, g.getLevel());
                    ps.setString(2, g.getFurnace().getLocation().getWorld().getName());
                    ps.setInt(3, g.getFurnace().getLocation().getBlockX());
                    ps.setInt(4, g.getFurnace().getLocation().getBlockY());
                    ps.setInt(5, g.getFurnace().getLocation().getBlockZ());               
                    ps.setString(6, g.getOwnerUUID().toString());
                    ps.setInt(7, g.getId());
                    
                    ps.executeUpdate();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
    }


Allerdings kann ich dir greade nicht genau sagen, wie sich das auf die Performance auswirkt.

Vielleicht kann ja jemand anderes diese Frage beantworten.
 
Oben