Игровой сервер на nodejs (пакеты по tcp, база - mongo, игровой логики - по минимуму).
"Игрок" из себя представляет объект с ресурсами (золото, серебро, вещи и т.д). Когда клиент подключается к серверу, нахожу игрока в бд или в кэше и до момента отключения - игрок "принадлежит" этому подключению.
Пока только начал писать, но есть желание использовать cluster, чтобы в дальнейшем не мучится с масштабированием.
И сразу возникает вопрос - как красиво организовать обмен ресурсами между игроками в разных worker'ах?
Самая простая ситуация, когда это нужно:
К серверу подключаются 2 игрока (Женя и Маша), их подключения обрабатывают разные worker'ы. С клиента Жени приходит пакет: "Я хочу купить у Маши 5 объектов 'овца' за 15 золотых".
Каким образом отобрать у Маши 5 овец и дать ей 15 золотых, а у Жени - наоборот, с учетом валидации, сохранения в бд, возможной внезапной остановкой сервера и т.д.?
Поиск по интернету ничего особенного не дал: в лучшем случае, статьи на уровне "Создали worker, отправили сообщение".
Пока вижу такие варианты:
- Все игроки в mongo, никакого кэша, общение только через бд.
- - медленно
- - неудобно
- + просто
- игроки в redis или memcache, все действия клиентов - с этими записями
- - игроки - сложные объекты
- - долго сериализовать - десериализовать
- + общий пул игроков, нет нужды в синхронизации
- игроки в локальных кэшах на worker'ах, общаются через самописные транзакции через сам cluster, rabbitmq или что-то подобное
- - тяжело разруливать транзакции (их еще и написать нужно)
- - нужно уметь перетаскивать игроков с одного кэша на другой (при перелогине)
- + быстро
- + не нужна сериализация игроков
Попробовал на коленке написать простенькие транзакции:
Каждый ресурс игрока обернут в класс MarkedValue, который умеет "резервировать" часть ресурса для транзакции.
Если игрок хочет обменяться ресурсом с другим игроком:
- Просит основной поток создать транзакцию
- Говорит основному потоку, что нужно зарезервировать у себя и другого игрока такое-то количество таких-то ресурсов
- Если хоть один ресурс не удалось зарезервировать - основной поток сообщает об откате транзакции
- Если все ресурсы зарезервированы - применяем транзакцию и удаляем её из списка
В целом, такой подход не допустит исчезновения ресурсов при внештатном выключении сервера. Но для каждого типа ресурсов (число, список объектов и т.д.) придется писать свой MarkedValue.
'use strict';
class MarkedValue
{
constructor(value)
{
this.innerValue = value;
this.reserved = new Map();
this.appended = new Map();
this.reservedSum = 0;
}
get value()
{
return this.innerValue - this.reservedSum;
}
reserve(transactionId, amount)
{
if (this.value < amount)
return false;
if (this.reserved.has(transactionId))
this.reserved.set(transactionId, this.reserved.get(transactionId) + amount);
else
this.reserved.set(transactionId, amount);
this.reservedSum += amount;
return true;
}
append(transactionId, amount)
{
if (amount <= 0)
return false;
if (this.appended.has(transactionId))
this.appended.set(transactionId, this.appended.get(transactionId) + amount);
else
this.appended.set(transactionId, amount);
return true;
}
apply(transactionId)
{
let reservedAmount = this.reserved.get(transactionId);
if (reservedAmount !== undefined)
{
this.reservedSum -= reservedAmount;
this.innerValue -= reservedAmount;
this.reserved.delete(transactionId);
}
let appendedAmount = this.appended.get(transactionId);
if (appendedAmount !== undefined)
{
this.innerValue += appendedAmount;
this.appended.delete(transactionId);
}
}
rollback(transactionId)
{
let reservedAmount = this.reserved.get(transactionId);
if (reservedAmount !== undefined)
{
this.reservedSum -= reservedAmount;
this.reserved.delete(transactionId);
}
this.appended.delete(transactionId);
}
}
module.exports = MarkedValue;