Організація правильного кешування даних (PlayerInfo) в RAM

Технічний посібник зі створення високопродуктивної архітектури кешування в оперативній пам'яті за допомогою масиву PlayerInfo в Pawn для зниження дискового навантаження I/O.

20.05.2026 Українська

Організація правильного кешування даних (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 — це єдиний професійний спосіб створення модів під високий онлайн. Дозвольте оперативній пам'яті робити швидку та часту роботу, а базу даних використовуйте виключно як надійний довгостроковий архів.

Схожі статті

Захист сервера SA:MP від Flood-атак та нелегітимних пакетів на рівні Pawn

Технічний посібник із налаштування захисту серверів SA:MP від мережевого флуду, злому діалогів та нелегального спавну об'єктів за допомогою Pawn.RakNet та Nex-AC.

Читати далі

Виправлення багу з «невидимками» та розсинхронізацією віртуальних світів

Технічний посібник з усунення багу з невидимими гравцями та розсинхронізації віртуальних світів у SA:MP/CRMP за допомогою правильного використання інструментів Incognito Streamer.

Читати далі

Борьба з витоками пам'яті (Memory Leaks) в AMX-скриптах

Технічний посібник із пошуку та усунення витоків пам'яті в серверних скриптах SA:MP/CRMP AMX, фокусуючись на очищенні кешу MySQL, об'єктах Стрімера та стеку.

Читати далі