Организация правильного кэширования данных (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, объектах Стримера и стеке.

Читать далее