Організація правильного кешування даних (PlayerInfo) в RAM
Однією з найпоширеніших архітектурних помилок при розробці модів у SA:MP/CRMP є відправка SQL-запитів на запис до бази даних при кожній зміні стану гравця. Видача грошей, купівля зброї, отримання досвіду, зміна фракційного рангу — якщо на кожну з цих подій ваш мод реагує викликом важкого запиту UPDATE, сервер дуже швидко почне лагати.
При онлайні у 150–200 гравців кількість дрібних транзакцій за секунду починає обчислюватися сотнями. Дискова підсистема хостингу (навіть швидкі NVMe SSD) перевантажується операціями введення-виведення (I/O). Як результат — сервер зависає, а у гравців різко зростає пінг. У цій статті ми розберемо, як організувати правильне кешування даних через структуру PlayerInfo в оперативній пам'яті (RAM) сервера та мінімізувати навантаження на хостинг.
Концепція кешування: RAM як основне джерело правди
Оперативна пам'ять працює в тисячі разів швидше за будь-який фізичний диск. Правильний підхід до архітектури ігрового моду будується на простому правилі:
Дані гравця повинні зчитуватися з бази даних строго один раз — при авторизації, і записуватися назад у базу тільки при виході гравця або за глобальним таймером автозбереження.
Весь інший час, поки гравець перебуває на сервері, будь-які маніпуляції з його рівнем, грошима або інвентарем повинні відбуватися виключно всередині оперативної пам'яті сервера — у спеціальному масиві (кеші).
Крок 1: Правильне структурування змінних (масив PlayerInfo)
Для створення кешу в Pawn використовується перерахування (enum) і глобальний двовимірний масив, розмір якого жорстко прив'язаний до максимальної кількості слотів сервера (параметр MAX_PLAYERS).
enum E_PLAYER_DATA
{
pID,
pName[MAX_PLAYER_NAME],
pLevel,
pMoney,
pAdminLevel,
pFraction,
bool:pIsLoggedIn
}
new PlayerInfo[MAX_PLAYERS][E_PLAYER_DATA];
При підключенні та відключенні гравця цей кеш обов'язково потрібно очищати, щоб дані старого гравця не "перейшли" новачкові, який зайшов під тим же ID:
public OnPlayerDisconnect(playerid, reason)
{
SavePlayerAccount(playerid);
PlayerInfo[playerid][pID] = 0;
PlayerInfo[playerid][pName][0] = EOS;
PlayerInfo[playerid][pLevel] = 0;
PlayerInfo[playerid][pMoney] = 0;
PlayerInfo[playerid][pAdminLevel] = 0;
PlayerInfo[playerid][pFraction] = 0;
PlayerInfo[playerid][pIsLoggedIn] = false;
return 1;
}
Крок 2: Створення функції асинхронного збереження
Функція збереження повинна агрегувати всі змінені за час гри параметри з масиву PlayerInfo і однією командою відправляти їх до бази даних через асинхронний потік mysql_tquery:
stock SavePlayerAccount(playerid)
{
if(!PlayerInfo[playerid][pIsLoggedIn]) return 0;
new query[256];
format(query, sizeof(query),
"UPDATE `users` SET `level` = %d, `money` = %d, `admin` = %d, `fraction` = %d WHERE `id` = %d",
PlayerInfo[playerid][pLevel],
PlayerInfo[playerid][pMoney],
PlayerInfo[playerid][pAdminLevel],
PlayerInfo[playerid][pFraction],
PlayerInfo[playerid][pID]
);
mysql_tquery(db_handle, query);
return 1;
}
Крок 3: Налаштування глобального автозбереження (раз на 15 хвилин)
Щоб захистити проєкт від втрати прогресу гравців у разі раптового вимкнення сервера (наприклад, при краші моду або знеструмленні дата-центру), необхідно налаштувати циклічне автозбереження.
Увага: Ніколи не зберігайте всіх гравців в одну й ту саму секунду! Якщо на сервері грає 300 осіб, і таймер одночасно викличе 300 важких SQL-запитів, фоновий потік MySQL перевантажиться, що призведе до затримок викликів інших запитів. Збереження потрібно розподіляти у часі.
Реалізація "м'якого" розподіленого автозбереження:
public OnGameModeInit()
{
SetTimer("GlobalAutoSave", 60000, true);
return 1;
}
forward GlobalAutoSave();
public GlobalAutoSave()
{
static current_chunk = 0;
for(new i = 0; i < MAX_PLAYERS; i++)
{
if(!IsPlayerConnected(i) || !PlayerInfo[i][pIsLoggedIn]) continue;
if(i % 15 == current_chunk)
{
SavePlayerAccount(i);
}
}
current_chunk++;
if(current_chunk >= 15) current_chunk = 0;
return 1;
}
Як це впливає на продуктивність хостингу?
Давайте порахуємо математику дискового навантаження:
| Архітектура моду | Кіл-ть запитів (200 онлайн, 1 година гри) | Навантаження на I/O диска |
|---|---|---|
| Без кешування (UPDATE при кожній зміні грошей, вбивстві, отриманні пейдею) | ~ 15 000 — 40 000 запитів | Критична. Викликає мікрофризи, лаги синхронізації, "забиває" канал. |
| З кешуванням в RAM (+ розподілене автозбереження раз на 15 хвилин) | ~ 800 — 1 000 запитів | Мінімальна. Споживання ресурсів диска падає на 95-98%. Ідеальний плавний TPS. |
Підсумок: Кешування даних у RAM — це єдиний професійний спосіб створення модів під високий онлайн. Дозвольте оперативній пам'яті робити швидку та часту роботу, а базу даних використовуйте виключно як надійний довгостроковий архів.