Publiczność: FiveM właściciele serwerów, skrypterzy, administratorzy
Bramka: Zastępować mysql-async z oxmysql bezpiecznie, przyspieszaj zapytania i modernizuj swoje użycie SQL.
Przeczytaj także:
- Optymalizacja serwera FiveM: Definitywny przewodnik na rok 2025 — https://fivemx.com/how-to-optimize-fivem-server-performance/
- Wzorce adapterów: ESX↔QBCore↔QBOX Eksporty, Zdarzenia i Modele Graczy — https://fivemx.com/adapter-patterns/
TL;DR
- Używać
oxmysql: przygotowane zapytania, API promise\/await, lepsza diagnostyka, wysoka wydajność. - Minimalne zmiany w kodzie: zamień
@param→?(pozycyjne) lub:name(nazwane) parametry; zastąpMySQL.Async.*wywołania zMySQL.*/exports.oxmysql:*. - Uruchom skrypty SQL “UP” poniżej (poprawki zestawów znaków\/indeksów) i zachowaj rollback pod ręką.
- Zweryfikuj za pomocą narzędzia micro‑benchmark na końcu, aby potwierdzić korzyści na swoim sprzęcie.
1) Lista kontrolna bezpieczeństwa przed lotem
- Pełna kopia zapasowa:
mysqldump --single-transaction yourdb > backup.sql. - Środowisko stagingowe odwzorowanie schematu produkcyjnego + podzbiór danych.
- Artifact i zależności: Aktualna kompilacja FXServer, najnowsza
oxmysql. - Okno przestoju dla przełączenia na produkcję (zwykle < 5 minut).
- Sondy stanu gotowe:
/players, przepływ logowania, operacje ekonomiczne, operacje garażu, operacje ekwipunku, sprawdzanie banów.
2) Instalacja i podłączenie oxmysql
2.1 server.cfg
# Stop using mysql-async default_prio 500 # ensure mysql-async # ← comment out or remove # Start oxmysql default_prio 50 ensure oxmysql # Connection string consumed by oxmysql set mysql_connection_string "mysql://user:pass@127.0.0.1:3306/yourdb?charset=utf8mb4" # Optional diagnostics set mysql_slow_query_warning 200 # log queries slower than 200ms set mysql_debug false # true for verbose logging during staging
Trzymać
mysql-asyncwyłączony, ale dostępny w folderze zasobów podczas fazy przejściowej (do szybkiego wycofania).
3) Mapowanie API: mysql‑async → oxmysql
mysql-async (starszy):
- Asynchroniczny:
MySQL.Async.fetchAll,MySQL.Async.fetchScalar,MySQL.Async.execute - Synchronizacja:
MySQL.Sync.fetchAll,MySQL.Sync.fetchScalar,MySQL.Sync.execute - Parametry:
@paramstyl tabeli jak{ ['@identifier']=identifier }
oxmysql (nowoczesny):
- Styl callback przez eksport:
exports.oxmysql:query|scalar|single|insert|update(sql, params, cb) - Promise/await przez globalny:
MySQL.query|scalar|single|insert|update.await(sql, params)i wywołania zwrotne bez await.await - Parametry: pozycyjne
?przez tablicę, lub nazwany:nameprzez obiekt
3.1 Typowe zamienniki
SELECT wiele
-- mysql-async
MySQL.Async.fetchAll(
'SELECT * FROM users WHERE identifier = @id',
{ ['@id'] = identifier },
function(rows) ... end
)
-- oxmysql (callback via export)
exports.oxmysql:query(
'SELECT * FROM users WHERE identifier = ?',
{ identifier },
function(rows) ... end
)
-- oxmysql (await)
local rows = MySQL.query.await(
'SELECT * FROM users WHERE identifier = ?',
{ identifier }
)
SELECT pojedynczy wiersz
-- mysql-async (fetchAll + rows[1])
-- oxmysql
local row = MySQL.single.await(
'SELECT * FROM users WHERE identifier = ?',
{ identifier }
)
SELECT skalarny (np. count, id)
-- mysql-async
-- oxmysql
local count = MySQL.scalar.await(
'SELECT COUNT(*) FROM owned_vehicles WHERE owner = ?',
{ owner }
)
INSERT (pobierz insertId)
-- mysql-async (execute)
-- oxmysql
local insertId = MySQL.insert.await(
'INSERT INTO notes (owner, text) VALUES (?, ?)',
{ cid, text }
)
UPDATE/DELETE (affectedRows)
-- mysql-async (execute)
-- oxmysql
local changed = MySQL.update.await(
'UPDATE users SET job = ?, job_grade = ? WHERE identifier = ?',
{ job, grade, identifier }
)
Transakcje (ręczny)
-- oxmysql manual transaction
MySQL.query.await('START TRANSACTION')
local ok = true
local r1 = MySQL.update.await('UPDATE users SET bank = bank - ? WHERE identifier = ? AND bank >= ?', { amount, fromId, amount })
local r2 = MySQL.update.await('UPDATE users SET bank = bank + ? WHERE identifier = ?', { amount, toId })
if r1 == 1 and r2 == 1 then
MySQL.query.await('COMMIT')
else
MySQL.query.await('ROLLBACK')
end
Niektóre frameworki udostępniają otoczki (np.,
ox_lib), które dodająMySQL.ready,.transaction, itp. Powyższe wywołania są bezpieczne bez dodatkowych otoczek.
4) Ściągawka dotycząca przygotowanych zapytań
Style parametrów
- mysql‑async (przestarzałe):
@namesymbole zastępcze z tabelą:{ ['@name']=value } - oxmysql (pozycyjne):
?symbole zastępcze z tablicą:{ value1, value2 } - oxmysql (nazwane):
:namesymbole zastępcze z obiekt:{ name = value }
Przykłady
-- Named params (recommended for readability)
local row = MySQL.single.await(
'SELECT * FROM users WHERE identifier = :id',
{ id = identifier }
)
-- IN (...) list
-- Build placeholders dynamically and pass a flat array
local ids = { 'cid1','cid2','cid3' }
local qs = ('?,' ):rep(#ids):sub(1,-2) -- "?, ?, ?"
local rows = MySQL.query.await('SELECT * FROM players WHERE citizenid IN ('..qs..')', ids)
-- JSON fields (MySQL 5.7+/MariaDB 10.2+)
local name = MySQL.scalar.await('SELECT JSON_UNQUOTE(JSON_EXTRACT(data, "$.name")) FROM players WHERE citizenid = ?', { cid })
Nie
- Używać przygotowane zapytania wszędzie (nigdy nie łącz ciągów znaków z danymi od użytkownika).
- Woleć parametry nazwane dla przejrzystości w złożonych zapytaniach.
- Dodaj LIMIT 1 podczas odczytu pojedynczej encji.
Unikaj
- Symbol wieloznaczny
SELECT *w gorących ścieżkach (wymagane kolumny projektu). - Zapytania N+1 na wiersz; grupuj w pakiety za pomocą
IN (...).
5) Skrypty migracji bazy danych „UP” (gotowe do uruchomienia)
Wybierz bloki pasujące do Twojego frameworka (ESX/QBCore) i serwera (MySQL 8+ lub MariaDB 10.4+). Najpierw uruchom na staging. .
5.1 Normalizacja zestawu znaków i kodowania (wszędzie UTF‑8)
(A) MySQL 8+ — zastąp yourdb raz
-- Force database default to utf8mb4 (emoji‑safe) ALTER DATABASE `yourdb` CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci; -- Convert common tables (extend list as needed) ALTER TABLE `users` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE `owned_vehicles` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE `players` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE `player_vehicles` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
(B) MariaDB 10.4+ — te same instrukcje są poprawne.
Dodaj inne popularne tabele (inventory, billing, phone, society, jobs) obecne na Twoim serwerze.
5.2 Indeksy ESX (bezpieczne zyski wydajności)
MySQL 8+
ALTER TABLE `users` ADD INDEX IF NOT EXISTS `idx_users_identifier` (`identifier`), ADD INDEX IF NOT EXISTS `idx_users_job` (`job`), ADD INDEX IF NOT EXISTS `idx_users_name` (`name`); ALTER TABLE `owned_vehicles` ADD UNIQUE INDEX IF NOT EXISTS `ux_owned_vehicles_plate` (`plate`), ADD INDEX IF NOT EXISTS `idx_owned_vehicles_owner` (`owner`);
MariaDB 10.4+
-- Drop first to be idempotent where IF NOT EXISTS is unavailable DROP INDEX IF EXISTS `idx_users_identifier` ON `users`; DROP INDEX IF EXISTS `idx_users_job` ON `users`; DROP INDEX IF EXISTS `idx_users_name` ON `users`; CREATE INDEX `idx_users_identifier` ON `users` (`identifier`); CREATE INDEX `idx_users_job` ON `users` (`job`); CREATE INDEX `idx_users_name` ON `users` (`name`); DROP INDEX IF EXISTS `ux_owned_vehicles_plate` ON `owned_vehicles`; DROP INDEX IF EXISTS `idx_owned_vehicles_owner` ON `owned_vehicles`; CREATE UNIQUE INDEX `ux_owned_vehicles_plate` ON `owned_vehicles` (`plate`); CREATE INDEX `idx_owned_vehicles_owner` ON `owned_vehicles` (`owner`);
5.3 QBCore/QBOX Indeksy
MySQL 8+
ALTER TABLE `players` ADD UNIQUE INDEX IF NOT EXISTS `ux_players_citizenid` (`citizenid`), ADD INDEX IF NOT EXISTS `idx_players_license` (`license`), ADD INDEX IF NOT EXISTS `idx_players_steam` (`steam`), ADD INDEX IF NOT EXISTS `idx_players_last_name` (`lastname`); ALTER TABLE `player_vehicles` ADD UNIQUE INDEX IF NOT EXISTS `ux_player_vehicles_plate` (`plate`), ADD INDEX IF NOT EXISTS `idx_player_vehicles_citizenid` (`citizenid`);
MariaDB 10.4+
DROP INDEX IF EXISTS `ux_players_citizenid` ON `players`; DROP INDEX IF EXISTS `idx_players_license` ON `players`; DROP INDEX IF EXISTS `idx_players_steam` ON `players`; DROP INDEX IF EXISTS `idx_players_last_name` ON `players`; CREATE UNIQUE INDEX `ux_players_citizenid` ON `players` (`citizenid`); CREATE INDEX `idx_players_license` ON `players` (`license`); CREATE INDEX `idx_players_steam` ON `players` (`steam`); CREATE INDEX `idx_players_last_name` ON `players` (`lastname`); DROP INDEX IF EXISTS `ux_player_vehicles_plate` ON `player_vehicles`; DROP INDEX IF EXISTS `idx_player_vehicles_citizenid` ON `player_vehicles`; CREATE UNIQUE INDEX `ux_player_vehicles_plate` ON `player_vehicles` (`plate`); CREATE INDEX `idx_player_vehicles_citizenid` ON `player_vehicles` (`citizenid`);
5.4 Opcjonalnie: ox_inventory (jeśli zainstalowano)
ALTER TABLE `ox_inventory` ADD INDEX IF NOT EXISTS `idx_inv_owner` (`owner`), ADD INDEX IF NOT EXISTS `idx_inv_type` (`type`); ALTER TABLE `ox_inventory_items` ADD INDEX IF NOT EXISTS `idx_items_inv_owner_name` (`inventory`, `owner`, `name`);
Dostosuj nazwy tabel, jeśli twój schemat się różni (niektóre konfiguracje używają
inventories/items).
6) Plan wycofania (Zero‑Panic)
6.1 Wycofanie kodu
- Cofnij zmiany w zasobach (zachowaj
legacy-mysql-asyncbranch). - W
server.cfgzamień:# ensure oxmysql ensure mysql-async - Uruchom ponownie FXServer lub dotknięte zasoby w kolejności zależności.
6.2 SQL wycofanie
- Jeśli tylko dodane indeksy: usuń je (patrz MariaDB blocks above — use
DROP INDEX IF EXISTS). - Jeśli zmieniono zestaw znaków/kolekcję i muszą cofnąć, przywrócić bazę danych i tabele:
ALTER DATABASE `yourdb` CHARACTER SET = utf8 COLLATE = utf8_general_ci; ALTER TABLE `users` CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; ALTER TABLE `owned_vehicles` CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; ALTER TABLE `players` CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; ALTER TABLE `player_vehicles` CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci;
Preferuj przywracanie z backup.sql instead of mass charset reversals when possible.
7) Procedura migracji od początku do końca (skryptowalna)
- Freeze wdraża, wykonaj kopię zapasową bazy danych.
- Zastosuj Sekcja 5 „UP” SQL na staging → weryfikuj → prod.
- Refaktoryzacja kodu commit: zamień wywołania (Sekcja 3) + style parametrów (Sekcja 4).
- Deploy,
ensure oxmysql, uruchom ponownie serwer. - Uruchom testy dymne (logowanie, odcinki wypłat, ekwipunek, pojawianie się/znikanie pojazdów, bany, pieniądze społeczne, przełączniki obowiązków w pracy).
- Obserwuj logi przez 15-30 minut (
mysql_slow_query_warningpomaga); adresuj wszelkie pominięte parametry lub niezgodności schematu.
8) Mikro-benchmarki (Przynieś własne liczby)
A tiny resource you can drop in to compare hot‑path queries on Twój sprzęt i zestaw danych.
fxmanifest.lua
fx_version 'cerulean' game 'gta5' server_script 'bench.lua'
bench.lua
local COUNT = 2000 -- adjust for your server
local function bench(name, fn)
local t0 = os.clock()
local ok, err = pcall(fn)
local dt = (os.clock() - t0) * 1000.0
print(('[bench] %s: %.2f ms %s'):format(name, dt, ok and '' or ('ERR: '..tostring(err))))
end
-- Hot path 1: ownership lookup
bench('SELECT single', function()
for i=1,COUNT do
local row = MySQL.single.await('SELECT owner FROM owned_vehicles WHERE plate = :p LIMIT 1', { p = ('TEST%04d'):format(i % 500) })
end
end)
-- Hot path 2: batched fetch
bench('SELECT batch IN', function()
local ids = {}
for i=1,100 do ids[#ids+1] = ('cid%04d'):format(i) end
local qs = ('?,' ):rep(#ids):sub(1,-2)
local rows = MySQL.query.await('SELECT citizenid, firstname, lastname FROM players WHERE citizenid IN ('..qs..')', ids)
end)
-- Hot path 3: update with guard
bench('UPDATE guarded', function()
for i=1,COUNT do
local changed = MySQL.update.await('UPDATE users SET bank = bank + :d WHERE identifier = :id AND bank >= 0', { d = 1, id = ('lic:%04d'):format(i % 500) })
end
end)
Jak uruchomić
- Umieść zasób w folderze (np.,
ox-bench/), addensure ox-benchdoserver.cfg. - Śledź konsolę serwera; wyniki są drukowane jako linie typu:
[bench] SELECT single: 134.21 ms. - Dla przed/po porównanie, uruchom raz z
mysql-async(w razie potrzeby dostosuj wywołania), a następnie zoxmysql.
What to look for
- Niższy całkowity czas ms na sekcję po migracji.
- Obniż opóźnienia P95/P99 w akcjach rozgrywki powiązanych z zapytaniami.
- Mniej ostrzeżeń o wolnych zapytaniach przez godzinę gry na żywo.
9) Rozwiązywanie problemów
P: Otrzymuję „no such export: query/single/…”.
A: oxmysql nie został uruchomiony wystarczająco wcześnie. Upewnij się ensure oxmysql jest powyżej zasobów, które go używają.
P: Błędy parametrów lub puste wyniki.
O: Prawdopodobnie zostawiłeś @param miejsca zastępcze. Zastąp przez ? Lub :name i odpowiednio przekaż tablicę/obiekt.
P: Blokady lub częściowe zapisy.
O: Owiń wieloetapowe salda/transfery w transakcję (patrz Sekcja 3), dodaj indeksy z Sekcji 5.
P: JSON ścieżka zwraca NULL.
O: Potwierdź, że Twój silnik obsługuje funkcje JSON (MySQL ≥5.7/MariaDB ≥10.2) i że typ kolumny to JSON (nie LONGTEXT).
P: Wolne działanie po migracji.
O: Sprawdź brakujące indeksy, EXPLAIN zapytanie, wybieraj tylko potrzebne kolumny i przejrzyj Podręcznik optymalizacji serwera.
10) Lista kontrolna przeglądu kodu (kopiuj/wklej)
- Brak konkatenacji ciągów znaków SQL; wszystkie zapytania sparametryzowane.
- Używać
.single/.scalarzLIMIT 1gdy wymagany jest tylko jeden wiersz/wartość. - Partia
IN (...)odczyty dla kolekcji. - Transakcje wokół wieloetapowych operacji pieniężnych/inwentaryzacyjnych.
- Indeks obecny dla każdego gorącego
WHERE/JOINcolumn. - Unikaj
SELECT *na ścieżkach krytycznych. - Loguj wolne zapytania; śledź głównych winowajców co tydzień.
Linki wewnętrzne
- Optymalizacja serwera FiveM: Definitywny przewodnik na rok 2025 — https://fivemx.com/how-to-optimize-fivem-server-performance/
- Wzorce adapterów: ESX↔QBCore↔QBOX Eksporty, Zdarzenia i Modele Graczy — https://fivemx.com/adapter-patterns/
Podziękowania
Utrzymywane przez fivemx.com. Mile widziane wkłady (prześlij różnice dodatkowych bezpiecznych indeksów lub pomocników opakowujących).
