N
N
nightrain9122015-11-22 16:13:25
JavaScript
nightrain912, 2015-11-22 16:13:25

How to synchronize objects in a multi-threaded application on node.js?

Game server on nodejs (tcp packages, base - mongo, game logic - to a minimum).
"Player" is an object with resources (gold, silver, things, etc.). When the client connects to the server, I find the player in the database or in the cache, and until the moment of disconnection, the player "belongs" to this connection.
So far, I just started writing, but there is a desire to use cluster, so that in the future I don’t suffer with scaling.
And the question immediately arises - how to beautifully organize the exchange of resources between players in different workers?
The simplest situation when it is needed:
2 players connect to the server (Zhenya and Masha), their connections are processed by different workers. A package arrives from Zhenya's client: "I want to buy 5 'sheep' objects from Masha for 15 gold pieces."
How to take away 5 sheep from Masha and give her 15 gold, and vice versa from Zhenya, taking into account validation, saving in the database, a possible sudden stop of the server, etc.?
Internet search did not give anything special: at best, articles at the level of "Created a worker, sent a message."
So far I see these options:

  1. All players in mongo, no cache, communication only through the database.
    • - slowly
    • - uncomfortable
    • + easy

  2. players in redis or memcache, all client actions - with these records
    • - players - complex objects
    • - long serialize - deserialize
    • + common pool of players, no need for synchronization

  3. players in local caches on workers, communicate through self-written transactions through the cluster itself, rabbitmq or something similar
    • - hard to resolve transactions (you also need to write them)
    • - you need to be able to drag players from one cache to another (when relogin)
    • + fast
    • + no need to serialize players


I tried to write simple transactions on my knee:
Each player's resource is wrapped in the MarkedValue class, which can "reserve" part of the resource for a transaction.
If a player wants to exchange a resource with another player:
  1. Asks the main thread to create a transaction
  2. Tells the main thread that you need to reserve such and such a number of such and such resources from yourself and another player
  3. If at least one resource could not be reserved, the main thread reports the rollback of the transaction
  4. If all resources are reserved - apply the transaction and remove it from the list

In general, this approach will not allow the disappearance of resources during an abnormal server shutdown. But for each type of resource (number, list of objects, etc.) you will have to write your own 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;

Answer the question

In order to leave comments, you need to log in

4 answer(s)
A
Alexander Lozovyuk, 2015-11-22
@aleks_raiden

Believe me, you will not hit the base for a very long time, especially in monga. So what can be done in the easiest way.

Y
yeti357, 2015-11-26
@yeti357

A good solution would be to have a common storage in memory (radish, memcache), and yes, you have to serialize, etc., there are no simple and quick solutions to such problems. If you are interested in interprocess communication, then there is process.send / process.on('message') for this, but this is only available for processes of the master<->child type, and I do not advise you to abuse it, with intensive exchange it will be bad for both the OS and your application.

O
OnYourLips, 2015-11-22
@OnYourLips

Create a transaction, change the data in the database.
I see no problems with speed (within the same reliability) and inconvenience. The method is the most convenient and reliable.

N
Nicholas, 2015-11-22
@ACCNCC

I made an exchange according to 1 option and it turned out like in steam)
If you need something like that, then through the database!
Why is it for you:
- slow
- uncomfortable
???

Didn't find what you were looking for?

Ask your question

Ask a Question

731 491 924 answers to any question