Optimierung von MySQL-Abfragen in Pawn: Server-Freezes erfolgreich beheben
Die Server-Engine von SA:MP/CRMP arbeitet nach einem strikten einzelthreadbasierten Prinzip (Single-Threaded). Das bedeutet, dass absolut alle Prozesse—Spielerbewegungen, Timer-Schleifen, Befehlsverarbeitungen, Anticheat-Prüfungen und Dateizugriffe—nacheinander in einem einzigen Hauptthread ausgeführt werden. Wenn eine Funktion in Ihrem Pawn-Skript 500 Millisekunden zur Ausführung benötigt, friert der gesamte Server buchstäblich für eine halbe Sekunde ein. Für die Spieler äußert sich dies in plötzlichen Ping-Spikes, verzögerten Animationen oder kurzen Rucklern.
Der häufigste Verursacher solcher Mikroruckler und Freezes ist eine inkorrekte Interaktion mit der MySQL-Datenbank. In diesem Artikel analysieren wir den Unterschied zwischen den Abfragetypen im BlueG MySQL-Plugin und zeigen, wie eine einzige unoptimierte Abfrage während der Autorisierung den gesamten Server lahmlegen kann.
Blockierende Abfragen (mysql_query) — Der größte Fehler des Entwicklers
In älteren Versionen des MySQL-Plugins (und leider auch in vielen veralteten Skripten) wird die Funktion mysql_query() zum Senden von SQL-Befehlen verwendet. Dies ist eine sogenannte synchrone (blockierende) Abfrage.
Wenn der Server ein mysql_query ausführt, passiert Folgendes:
- Der Hauptthread des Servers stoppt die Verarbeitung des Spielgeschehens.
- Der Server sendet die Abfrage über das Netzwerk an den MySQL-Server des Hostings.
- Der SA:MP-Server wartet, bis MySQL den passenden Eintrag in der Tabelle findet, verarbeitet und die Antwort zurücksendet.
- Erst nach Erhalt der Antwort wacht der Hauptthread wieder auf und führt das Spiel fort.
Ein katastrophales Beispiel: Ein Spieler betritt den Server mit dem Namen Ivan_Petrov. Das Skript sendet eine synchrone Abfrage: mysql_query(db_handle, "SELECT * FROM users WHERE name = 'Ivan_Petrov'");. Wenn die Account-Tabelle mittlerweile 50.000 Datensätze enthält und die Spalte name keinen Index besitzt, benötigt die Datenbank etwa 200–400 Millisekunden, um die gesamte Festplatte zu lesen. Während dieser Zeit ist der Server für die anderen 200 aktiven Spieler komplett blockiert.
Asynchrone Abfragen (mysql_tquery und mysql_pquery) — Die Rettung für die Server-TPS
Moderne Versionen des BlueG MySQL-Plugins (R39-R41+) bieten ein mächtiges Werkzeug: Asynchronität. Die Funktionen mysql_tquery (Threaded Query) und mysql_pquery (Parallel Query) verarbeiten die Last in separaten, parallelen Threads des Linux-Betriebssystems, ohne die eigentliche Spiel-Engine zu beeinträchtigen.
Bei der Verwendung des asynchronen Ansatzes ändert sich der Ablauf grundlegend:
- Der Hauptthread ruft
mysql_tqueryauf, übergibt die Aufgabe sofort an den parallelen Thread des Plugins und setzt das Spielgeschehen direkt fort (Spieler bewegen sich flüssig, Treffer registrieren ordnungsgemäß, die TPS bleiben stabil bei 50). - Der parallele Thread kommuniziert im Hintergrund mit der Datenbank, wartet auf die Antwort und verarbeitet die schwere SQL-Abfrage.
- Sobald die Daten bereitstehen, signalisiert das MySQL-Plugin dem Hauptthread das Ergebnis und ruft eine spezielle Antwortfunktion auf — den Callback, in dem wir dem Spieler sicher sein Level und sein Geld zuweisen können.
Praktisches Beispiel: Umstellung der Autorisierung auf asynchrone Abläufe
Wie man es NICHT machen sollte (Synchroner, blockierender Code):
public OnPlayerConnect(playerid)
{
new query[128], Cache:result;
GetPlayerName(playerid, query, MAX_PLAYER_NAME);
format(query, sizeof(query), "SELECT id FROM users WHERE name = '%s'", query);
result = mysql_query(db_handle, query);
if(cache_num_rows() > 0) {
ShowPlayerDialog(playerid, DIALOG_LOGIN, DIALOG_STYLE_PASSWORD, "Login", "Passwort eingeben:", "Einloggen", "Beenden");
} else {
ShowPlayerDialog(playerid, DIALOG_REG, DIALOG_STYLE_INPUT, "Registrierung", "Passwort ausdenken:", "Weiter", "Beenden");
}
cache_delete(result);
return 1;
}
Der RICHTIGE Ansatz (Asynchroner Code mit Callback):
public OnPlayerConnect(playerid)
{
new query[128], p_name[MAX_PLAYER_NAME];
GetPlayerName(playerid, p_name, sizeof(p_name));
format(query, sizeof(query), "SELECT id FROM users WHERE name = '%s'", p_name);
mysql_tquery(db_handle, query, "CheckPlayerAccount", "d", playerid);
return 1;
}
forward CheckPlayerAccount(playerid);
public CheckPlayerAccount(playerid)
{
if(!IsPlayerConnected(playerid)) return 1;
if(cache_num_rows() > 0) {
ShowPlayerDialog(playerid, DIALOG_LOGIN, DIALOG_STYLE_PASSWORD, "Login", "Passwort eingeben:", "Einloggen", "Beenden");
} else {
ShowPlayerDialog(playerid, DIALOG_REG, DIALOG_STYLE_INPUT, "Registrierung", "Passwort ausdenken:", "Weiter", "Beenden");
}
return 1;
}
Was ist der Unterschied zwischen mysql_tquery und mysql_pquery?
mysql_tquery: Erstellt einen einzelnen Hintergrund-Thread für Abfragen. Alle asynchronen Abfragen werden in einer einzigen Hintergrundwarteschlange abgelegt und strikt nacheinander ausgeführt. Dies garantiert, dass Abfragen in der exakten chronologischen Reihenfolge verarbeitet werden – wichtig z. B. wenn Sie Daten erst speichern und direkt danach wieder laden möchten. Ideal für 95 % aller Aufgaben.mysql_pquery: Erstellt einen Pool aus mehreren parallelen Threads. Die Abfragen werden gleichzeitig auf verschiedenen CPU-Kernen verarbeitet. Dies beschleunigt die Abfragezeiten, jedoch ist die Verarbeitungsreihenfolge nicht garantiert. Nutzen Siemysql_pquerydaher nur für rechenintensive Protokollierungen oder Log-Einträge (z. B. Kill-Logs oder Admin-Aktionen), bei denen die chronologische Reihenfolge für die Account-Logik unkritisch ist.
Die goldene Regel der Datenbank-Indizierung
Selbst eine asynchrone Abfrage kann Lags verursachen, wenn die Datenbank unvorteilhaft konfiguriert ist. Wenn ein Hintergrund-Thread 5 Sekunden für eine SQL-Abfrage benötigt, hängt der betroffene Spieler ewig am Spawnpunkt fest und wartet auf das Dialogfenster.
Um dies zu verhindern, fügen Sie unbedingt Indizes (INDEX / UNIQUE) für alle Felder hinzu, nach denen Sie filtern (WHERE). In der User-Tabelle müssen zwingend Indizes auf den Spalten name (Benutzername) und id liegen.
Befehl zur Optimierung via phpMyAdmin:
Öffnen Sie die SQL-Registerkarte Ihrer Datenbank im Hosting-Panel und führen Sie folgenden Befehl aus:ALTER TABLE `users` ADD INDEX (`name`);
Dies beschleunigt die Account-Suche beim Verbindungsaufbau um das Hundertfache und senkt die CPU-Last Ihrer Server-Hardware auf ein Minimum.