@nightrain912

Как синхронизировать объекты в многопоточном приложении на node.js?

Игровой сервер на nodejs (пакеты по tcp, база - mongo, игровой логики - по минимуму).
"Игрок" из себя представляет объект с ресурсами (золото, серебро, вещи и т.д). Когда клиент подключается к серверу, нахожу игрока в бд или в кэше и до момента отключения - игрок "принадлежит" этому подключению.

Пока только начал писать, но есть желание использовать cluster, чтобы в дальнейшем не мучится с масштабированием.
И сразу возникает вопрос - как красиво организовать обмен ресурсами между игроками в разных worker'ах?

Самая простая ситуация, когда это нужно:
К серверу подключаются 2 игрока (Женя и Маша), их подключения обрабатывают разные worker'ы. С клиента Жени приходит пакет: "Я хочу купить у Маши 5 объектов 'овца' за 15 золотых".
Каким образом отобрать у Маши 5 овец и дать ей 15 золотых, а у Жени - наоборот, с учетом валидации, сохранения в бд, возможной внезапной остановкой сервера и т.д.?

Поиск по интернету ничего особенного не дал: в лучшем случае, статьи на уровне "Создали worker, отправили сообщение".

Пока вижу такие варианты:

  1. Все игроки в mongo, никакого кэша, общение только через бд.
    • - медленно
    • - неудобно
    • + просто

  2. игроки в redis или memcache, все действия клиентов - с этими записями
    • - игроки - сложные объекты
    • - долго сериализовать - десериализовать
    • + общий пул игроков, нет нужды в синхронизации

  3. игроки в локальных кэшах на worker'ах, общаются через самописные транзакции через сам cluster, rabbitmq или что-то подобное
    • - тяжело разруливать транзакции (их еще и написать нужно)
    • - нужно уметь перетаскивать игроков с одного кэша на другой (при перелогине)
    • + быстро
    • + не нужна сериализация игроков


Попробовал на коленке написать простенькие транзакции:
Каждый ресурс игрока обернут в класс MarkedValue, который умеет "резервировать" часть ресурса для транзакции.
Если игрок хочет обменяться ресурсом с другим игроком:
  1. Просит основной поток создать транзакцию
  2. Говорит основному потоку, что нужно зарезервировать у себя и другого игрока такое-то количество таких-то ресурсов
  3. Если хоть один ресурс не удалось зарезервировать - основной поток сообщает об откате транзакции
  4. Если все ресурсы зарезервированы - применяем транзакцию и удаляем её из списка

В целом, такой подход не допустит исчезновения ресурсов при внештатном выключении сервера. Но для каждого типа ресурсов (число, список объектов и т.д.) придется писать свой 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;
  • Вопрос задан
  • 669 просмотров
Пригласить эксперта
Ответы на вопрос 4
Поверьте, вы очень еще долго не упретесь в базу, в монгу особенно. Так что можно делать самым простым путем.
Ответ написан
@yeti357
Хорошим решением будет иметь общее хранилище в памяти(редис, мемкэш), и да придётся сериализвать и тд, простых и быстрых решений таких задач не бывает. Если интересует межпроцессное взаимодействие то для этого есть process.send / process.on('message'), но это доступно только для процессов типа master<->child, и злоупотреблять этим не советую, при интенсивном обмене тут уже плохо будет и ОС и вашему приложению.
Ответ написан
OnYourLips
@OnYourLips
Создаете транзакцию, меняете данные в базе.
Не вижу проблем со скоростью (в пределах аналогичной надежности) и неудобством. Способ самый удобный и надежный.
Ответ написан
ACCNCC
@ACCNCC
Делаю игры!
Я делал обмен по 1 варианту и получилось как в steam)
Если вам нужно что-то подобное, то через бд!

Почему он для Вас:
- медленный
- неудобный
???
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы