2011-01-15 6 views
2

Ich arbeite in einem Computercafe und habe ein System hier (Smartlaunch), das die Spiellizenzen verfolgt. Ich habe ein Programm geschrieben, das mit diesem System verbunden ist (eigentlich mit seiner Backend-MySQL-Datenbank). Das Programm soll auf einem Client-PC ausgeführt werden und (1) die Datenbank abfragen, um eine nicht verwendete Lizenz aus dem verfügbaren Pool auszuwählen, und dann (2) diese Lizenz als vom Client-PC verwendet markieren.Java: Simultane MySQL-Abfragen von mehreren Clients synchronisieren

Das Problem ist, ich habe einen Nebenläufigkeitsfehler. Das Programm soll gleichzeitig auf mehreren Rechnern gestartet werden, und wenn dies geschieht, versuchen einige Maschinen oft dieselbe Lizenz zu erwerben. Ich denke, das liegt daran, dass die Schritte (1) und (2) nicht synchronisiert sind, dh ein Programm bestimmt, dass Lizenz # 5 verfügbar ist und es auswählt, aber bevor es # 5 als eine andere Kopie des Programms auf einem anderen PC markieren kann versucht, dieselbe Lizenz zu bekommen.

Ich habe versucht, dieses Problem zu lösen, indem ich Transaktionen und Tabellen sperren, aber es scheint keinen Unterschied zu machen - mache ich das richtig? Hier folgt den Code in Frage:

public LicenseKey Acquire() throws SmartLaunchException, SQLException { 
    Connection conn = SmartLaunchDB.getConnection(); 
    int PCID = SmartLaunchDB.getCurrentPCID(); 

    conn.createStatement().execute("LOCK TABLE `licensekeys` WRITE"); 

    String sql = "SELECT * FROM `licensekeys` WHERE `InUseByPC` = 0 AND LicenseSetupID = ? ORDER BY `ID` DESC LIMIT 1"; 
    PreparedStatement statement = conn.prepareStatement(sql); 
    statement.setInt(1, this.id); 
    ResultSet results = statement.executeQuery(); 

    if (results.next()) { 
     int licenseID = results.getInt("ID"); 
     sql = "UPDATE `licensekeys` SET `InUseByPC` = ? WHERE `ID` = ?"; 
     statement = conn.prepareStatement(sql); 
     statement.setInt(1, PCID); 
     statement.setInt(2, licenseID); 
     statement.executeUpdate(); 
     statement.close(); 
     conn.commit(); 
     conn.createStatement().execute("UNLOCK TABLES"); 
     return new LicenseKey(results.getInt("ID"), this, results.getString("LicenseKey"), results.getInt("LicenseKeyType")); 
    } else { 
     throw new SmartLaunchException("All licenses of type " + this.name + "are in use"); 
    } 
} 

Antwort

3

Sie müssen zwei Dinge tun:

  • Wickeln Sie Ihren Code in einer Transaktion
  • Verwenden Sie SELECT ... (autocommit Freigeben von Sperren sofort zu vermeiden) FOR UPDATE und mysql geben Sie die Sperre Sie benötigen (veröffentlicht am begehen)

SELECT ... FOR UPDATE ist besser als LOCK TABLE wie nur irgend möglich mit Sperren auf Zeilenebene erhalten, indem kann, anstatt automatisch die gesamte Tabelle

+0

Ja das sieht so aus, als könnte es leicht ein Autocommit "Bug" sein. Wenn Sie die Anweisung "LOCK TABLES" ausführen und sie autokommt, wird die Tabelle für eine Nanosekunde gesperrt und bei der Ausführung der Transaktion freigegeben. –

+0

wurde in der geposteten Einreichung nicht angezeigt, aber die SmartLaunchDB.getConnection() -Methode setzt autocommit auf false. Sie haben Recht, dass die Auswahl für das Update wahrscheinlich ein besserer Sperrmechanismus ist, aber danke dafür. –

+0

Nun, es ist nicht genug, Autocommit auszuschalten. Sie müssen Ihre Transaktion noch klar abgrenzen, damit die Sperre gültig ist, bis das Update abgeschlossen ist. –

1

Nach dem online manual, die korrekte Syntax zur Verriegelung ist:

LOCK TABLES ... 

und Sie haben

LOCK TABLE ... 

, aber Sie haben keine Fehlerüberprüfung. Daher können Sie das Schloss wahrscheinlich nicht erreichen, und das ignoriert es still.

FWIW, würde ich Ihren Bereinigungscode setzen (UNLOCK TABLES, conn.commit(), etc.) in einem finally Block, um sicherzustellen, dass Sie immer richtig im Fall einer Ausnahme aufzuräumen.

Wie es aussieht, verlieren Sie möglicherweise Datenbankverbindungshandles und geben die Sperre nicht frei, wenn keine freie Lizenz vorhanden ist.

+0

Das ist auch, was ich bemerkt habe. Obwohl das Handbuch eine LOCK TABLE im Alias-Beispiel verwendet. – extraneon

+0

Ja, ich muss überprüfen, ob diese Syntax tatsächlich erlaubt ist. Ansonsten scheint die Lock-Erfassung korrekt zu sein. Das Fehlen einer Fehlerprüfung ist jedoch immer noch ein Problem. – Alnitak

+0

Sowohl TABELLE als auch TABELLEN scheinen akzeptabel zu sein –

0

Ich würde vorschlagen, nur eine Update-Anweisung zu tun und zu überprüfen, wie viele Zeilen aktualisiert wurden. Ich werde es in psudo-Code schreiben.

Wenn dies erfolgreich ist, können Sie den Lizenzschlüssel/die ID durch Auswählen abrufen.

0
Verriegelungs

Wie so oft ist OP ein Idiot. Der Code, den ich gepostet habe, funktionierte tatsächlich, aber ich habe gerade eine doppelte Zeile in der Datenbank entdeckt - ich nehme an, jemand hat aus Versehen die gleiche Lizenz zweimal eingegeben. Dies führte mich zu der Annahme, dass ein Nebenläufigkeitsfehler, den ich behoben hatte (durch Einführung von Tabellensperren), immer noch nicht behoben war.

Danke für den allgemeinen Hinweis, ich habe eine bessere Ausnahmebehandlung bei dieser Methode eingeführt.

+0

schlagen Sie vor, dass Sie dem Lizenz-ID-Feld einen eindeutigen Index geben - dies stellt sicher, dass Sie dasselbe nicht zweimal eingeben können, sonst erhalten Sie einen Fehler, wenn Sie es versuchen. – Alnitak

+0

Die Datenbank ist Teil eines Drittanbieterprodukts. Während ich zustimme, dass es eine einzigartige Beschränkung haben sollte, bin ich nicht scharf darauf, meine Finger in Software zu stecken, zu der ich den Code nicht habe. Ich habe keine Ahnung, ob SmartLaunch einen solchen SQL-Fehler korrekt behandelt, wenn jemand versucht, doppelte Daten einzugeben. –

Verwandte Themen