MySQL asíncrono y oxmysql

De mysql-async a oxmysql: Migración segura y procesamiento de consultas…

Audiencia: Propietarios, programadores y mantenedores de servidores FiveM
Meta: Reemplazar mysql-async con oxmysql de forma segura, acelere las consultas y modernice su uso de SQL.

Lea también:


Resumen

  • Usar oxmysql:declaraciones preparadas, API de promesa/espera, mejores diagnósticos, sólido desempeño.
  • Cambios mínimos en el código: intercambio @param? (posicional) o :name parámetros (con nombre); reemplazar MySQL.Async.* llamadas con MySQL.*/exports.oxmysql:*.
  • Ejecute los scripts SQL “UP” A continuación (correcciones de conjunto de caracteres/índice) y mantener el revertir práctico.
  • Verificar con el arnés de micro-benchmark al final para confirmar las victorias en tu hardware.

1) Lista de verificación de seguridad previa al vuelo

  1. Copia de seguridad completa: mysqldump --single-transaction yourdb > backup.sql.
  2. Entorno de ensayo esquema de producción reflejado + subconjunto de datos.
  3. Artefacto y dependencias:Compilación actual de FXServer, última oxmysql.
  4. Ventana de tiempo de inactividad para cambio de producto (generalmente < 5 minutos).
  5. Sondas de salud listo: /players, flujo de inicio de sesión, operaciones económicas, operaciones de garaje, operaciones de inventario, controles de prohibición.

2) Instalar y cablear oxmysql

2.1 servidor.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

Mantener mysql-async deshabilitado pero disponible en su carpeta de recursos durante la fase de preparación (para una rápida reversión).

3) Mapeo de API: mysql‑async → oxmysql

mysql-async (legado):

  • Asíncrono: MySQL.Async.fetchAll, MySQL.Async.fetchScalar, MySQL.Async.execute
  • Sincronizar: MySQL.Sync.fetchAll, MySQL.Sync.fetchScalar, MySQL.Sync.execute
  • Parámetros: @param mesas de estilo como { ['@identifier']=identifier }

oxmysql (moderno):

  • Estilo de devolución de llamada a través de exportar: exports.oxmysql:query|scalar|single|insert|update(sql, params, cb)
  • Promesa/espera vía global: MySQL.query|scalar|single|insert|update.await(sql, params) y devoluciones de llamadas sin espera sin .await
  • Parámetros: posicional ? a través de una matriz, o nombrado :name vía objeto

3.1 Reemplazos comunes

SELECCIONE muchos

-- 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 }
)

SELECCIONAR una sola fila

-- mysql-async (fetchAll + rows[1])

-- oxmysql
local row = MySQL.single.await(
 'SELECT * FROM users WHERE identifier = ?',
 { identifier }
)

SELECCIONAR escalar (por ejemplo, contar, identificar)

-- mysql-async

-- oxmysql
local count = MySQL.scalar.await(
 'SELECT COUNT(*) FROM owned_vehicles WHERE owner = ?',
 { owner }
)

INSERTAR (obtener insertId)

-- mysql-async (execute)

-- oxmysql
local insertId = MySQL.insert.await(
 'INSERT INTO notes (owner, text) VALUES (?, ?)',
 { cid, text }
)

ACTUALIZAR/ELIMINAR (filas afectadas)

-- mysql-async (execute)

-- oxmysql
local changed = MySQL.update.await(
 'UPDATE users SET job = ?, job_grade = ? WHERE identifier = ?',
 { job, grade, identifier }
)

Actas (manual)

-- 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

Algunos marcos exponen envoltorios (por ejemplo, ox_lib) que añaden MySQL.ready, .transaction, etc. Las llamadas anteriores son seguras sin envoltorios adicionales.

4) Hoja de referencia para declaraciones preparadas

Estilos de parámetros

  • mysql‑async (heredado): @name marcadores de posición con una tabla: { ['@name']=value }
  • oxmysql (posicional): ? marcadores de posición con un formación: { value1, value2 }
  • oxmysql (nombrado): :name marcadores de posición con un objeto: { name = value }

Ejemplos

-- 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 })

Hacer

  • Usar declaraciones preparadas en todas partes (nunca concatene cadenas de entrada del usuario).
  • Preferir parámetros nombrados para mayor claridad en afirmaciones complejas.
  • Agregar LÍMITE 1 al leer una sola entidad.

Evitar

  • Comodín SELECT * en caminos calientes (el proyecto necesitaba columnas).
  • Consultas N+1 por fila; lote con IN (...).

5) Scripts de migración de base de datos “UP” (listos para ejecutar)

Seleccione los bloques compatibles con su framework (ESX/QBCore) y servidor (MySQL 8+ o MariaDB 10.4+). Ejecútelos primero en la configuración.

5.1 Normalizar el conjunto de caracteres y la intercalación (UTF‑8 en todas partes)

(A) MySQL 8+ - reemplazar yourdb una vez

-- 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+ —Las mismas afirmaciones son válidas.

Agregue otras tablas activas (inventario, facturación, teléfono, sociedad, trabajos) tal como están presentes en su servidor.

5.2 Índices ESX (el rendimiento seguro es lo mejor)

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 Índices QBCore/QBOX

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 Opcional: ox_inventory (si está instalado)

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`);

Ajuste los nombres de las tablas si su esquema es diferente (algunas configuraciones utilizan inventories / items).

6) Plan de reversión (sin pánico)

6.1 Reversión de código

  1. Revierte los cambios en tus recursos (mantén un legacy-mysql-async rama).
  2. En server.cfg intercambio: # ensure oxmysql ensure mysql-async
  3. Reinicie FXServer o los recursos afectados en orden de dependencia.

6.2 Reversión de SQL

  • Si solo tu índices añadidos:déjalos caer (ver el MariaDB bloques arriba — uso DROP INDEX IF EXISTS).
  • Si usted cambio de conjunto de caracteres/intercalación y debe deshacer, revertir la base de datos y las tablas:
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;

Prefiero restaurar desde copia de seguridad.sql en lugar de inversiones masivas de caracteres cuando sea posible.

7) Procedimiento de migración de extremo a extremo (programable)

  1. Congelación de despliegues, realizar una copia de seguridad de la base de datos.
  2. Aplicar Sección 5 SQL “UP” en puesta en escena → verificar → prod.
  3. Refactorización del código de confirmación: reemplazar llamadas (Sección 3) + estilos de parámetros (Sección 4).
  4. Desplegar, ensure oxmysql, reiniciar el servidor.
  5. Correr pruebas de humo (inicio de sesión, cheques de pago, inventario, aparición/desaparición de vehículos, prohibiciones, dinero de la sociedad, alternancia de deberes laborales).
  6. Observe los registros durante 15 a 30 minutos (mysql_slow_query_warning ayuda); solucione cualquier parámetro faltante o desajuste del esquema.

8) Micro-Benchmarks (Traiga sus propios números)

Un pequeño recurso que puedes usar para comparar consultas de rutas activas en su hardware y conjunto de datos.

fxmanifest.lua

fx_version 'cerulean'
game 'gta5'
server_script 'bench.lua'

banco.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)

Cómo correr

  1. Coloque el recurso en una carpeta (por ejemplo, ox-bench/), agregar ensure ox-bench a server.cfg.
  2. Consola del servidor Tail; los resultados se imprimen como líneas como: [bench] SELECT single: 134.21 ms.
  3. Para una antes/después comparación, ejecutar una vez con mysql-async (ajustar las llamadas si es necesario), luego con oxmysql.

Qué buscar

  • Menor cantidad total de ms por sección después de la migración.
  • Menor latencia P95/P99 en acciones de juego vinculadas a consultas.
  • Menos advertencias de consultas lentas durante una hora de juego en vivo.

9) Solución de problemas

P: Recibo el mensaje “no existe dicha exportación: consulta/única/…”.
A: oxmysql no se inicia con la suficiente antelación. Asegúrese ensure oxmysql está por encima de los recursos que lo utilizan.

Q: Errores de parámetros o resultados vacíos.
A: Probablemente lo mantuviste @param marcadores de posición. Reemplazar con ? o :name y pasar una matriz/objeto en consecuencia.

P: Bloqueos o escrituras parciales.
A: Envuelva saldos/transferencias de varios pasos en una transacción (consulte la Sección 3) y agregue índices de la Sección 5.

P: La ruta JSON devuelve NULL.
A: Confirme que su motor admita funciones JSON (MySQL ≥5.7/MariaDB ≥10.2) y que el tipo de columna sea JSON (no LONGTEXT).

P: Lento después de la migración.
A: Verifique los índices faltantes, EXPLAIN su consulta, el proyecto solo necesita columnas y revise el Manual de optimización del servidor.

10) Lista de verificación de revisión de código (copiar y pegar)

  • No hay SQL concatenado de cadenas; todas las consultas están parametrizadas.
  • Usar .single/.scalar con LIMIT 1 cuando solo se requiere una fila/valor.
  • Lote IN (...) lee para colecciones.
  • Transacciones en torno a operaciones de dinero/inventario de múltiples pasos.
  • Índice presente para cada hot WHERE/JOIN columna.
  • Evitar SELECT * en caminos calientes.
  • Registrar consultas lentas; realizar un seguimiento semanal de los principales infractores.

Enlaces internos


Créditos
Mantenido por fivemx.com. Se agradecen las contribuciones (envíe comparaciones de índices seguros adicionales o ayudantes de wrapper).