NodeJS:使用异步验证避免竞争条件

时间:2018-03-24 12:21:57

标签: node.js socket.io race-condition

我有一个问题,我目前处于一种情况,我有一个解决方案,但我不太确定它是否100%解决了手头的问题,因为我没有编写可以验证我的解决方案的测试。

我会喜欢你对这个问题的看法,也许是对更优雅的解决方案的建议,甚至可能是一种完全避免这个问题的方法。

这是:

我正在创建一个游戏,您可以创建或加入开放的房间/游戏。

用户界面中有一个游戏列表,当您点击游戏时,您尝试加入该游戏。

每个游戏都有一个赌注(您赢或输的信用额度),创作者设置的任何人都必须匹配。

在服务器端,在我让玩家真正加入房间之前,我必须确认他的信用余额足以匹配他加入的游戏的赌注。这将通过API调用。

现在,如果两个玩家一次加入游戏,我们可以说第一个玩家加入的验证需要3秒,但第二个玩家的验证只需要1秒。

由于房间是1比1,如果其他人已经做过,我不能让玩家加入。

我可以通过检查游戏中的玩家是否已经完成此操作:

// game already full 
if (game.p2) {
    return socket.emit("join_game_reply", {
        err: "Someone else already joined."
    })
}

但是,手头的问题是,在检查之后,我必须验证余额。

所以我们得到这样的东西:

socket.on("join_game", data => {
    const game = openGames[data.gameId}

    // game already full 
    if (game.p2) {
        return socket.emit("join_game_reply", {
            err: "Someone else already joined."
        })
    }

    // check if users balance is sufficient to match bet of room creator
    verifyUserBalance(socket.player, game.bet)
        .then(sufficient => {
            if(sufficient){
              // join game
              game.p2 = socket.player
            }
        })
})

这里的问题:

如果当玩家X点击join游戏开启时,验证开始,但在验证playerY加入并在playerX之前完成验证并因此设置为game.p2时。不久后完成了playerX的验证,然后服务器继续将game.p2设置为playerX,让playerY的UI状态为ingame,即使在服务器上他已经不在了。

我的解决方案是在验证后再次进行检查:

socket.on("join_game", data => {
        const game = openGames[data.gameId}

        // game already full 
        if (game.p2) {
            return socket.emit("join_game_reply", {
                err: "Someone else already joined."
            })
        }

        // check if users balance is sufficient to match bet of room creator
        verifyUserBalance(socket.player, game.bet)
            .then(sufficient => {
                if(sufficient){
                  // join game
                  if (game.p2) {
                      return socket.emit("join_game_reply", {
                         err: "Someone else already joined."
                      })
                      game.p2 = socket.player
                  }

                }
            })
    })

我认为这是有效的原因是因为nodeJS是单线程的,因此我可以确保在验证后我只允许玩家加入,如果没有其他人同时加入。

写完之后我真的觉得它会起作用,所以如果你看到任何错误,请让我犯错误!非常感谢您抽出宝贵时间!

2 个答案:

答案 0 :(得分:2)

您的代码可以运行,但我认为这是短期的自举,您将不得不在中期进行更改。

它会起作用

一个。如果你只有一台服务器。

B中。如果您的服务器没有崩溃

℃。如果您只有一个同步操作(此处为game.p2 = socket.player

甲。多台服务器

为了扩大您的基础设施,我担心它不会起作用。

您不应该使用nodejs变量(如openGames)来存储数据,而是从缓存数据库中检索它们(如redis)。这个redis数据库将是你唯一的事实来源。

B中。如果服务器崩溃

如果您的服务器崩溃(出于任何原因,例如完整磁盘......),将会发生同样的问题。您将丢失存储在nodejs变量中的所有数据。

℃。多个动作

如果您想在工作流程中添加一个操作(例如将下注金额放入托管),则需要在此操作(以及房间加入失败)时捕获失败并确保存在全部 - 或者没有机制(托管+加入或没有)。

您可以在代码中管理它,但它会变得非常复杂。

交易

在处理资金+行动时,我认为您应该使用数据库的交易功能。我会使用例如Redis Transactions

答案 1 :(得分:0)

您需要在服务器上进行“验证并加入”原子操作,以便其他任何人都不会导致竞争条件。解决方案的方法有很多种。最佳解决方案只会影响特定游戏,而不会影响加入其他游戏的过程。

这是一个想法:

  1. 创建“临时加入游戏”的方法。这将基本上保留您在游戏中的位置,然后您检查用户是否验证游戏。这可以防止其他人加入您第一次参加游戏并正在进行验证。

  2. 当其他人进入临时加入游戏时,但游戏已经有临时用户,加入功能可以返回尚未解决的承诺。如果先前的临时联接验证并完成,则此承诺将拒绝,因为游戏已满。如果其他临时联接无法验证,那么请求临时联接的第一个将解决,然后可以继续进行验证过程。

  3. 如果第二个用户正确验证并转换为已完成的加入游戏,它将拒绝任何其他等待此游戏的临时加入的承诺。如果验证失败,则返回步骤2,下一次等待有机会。

  4. 通过这种方式,每个游戏基本上都有一群用户等待进入游戏。只要游戏中没有经过验证的用户,队列就会挂起,所以每当有人不验证时,队列中的下一个队员就会加入。

    出于性能和用户体验的原因,您可能希望在队列中等待时实现超时,并且您可能希望限制队列中可以有多少用户(可能没有必要让100个用户进入队列因为它们不太可能都无法验证)。

    重要的是要了解验证和连接需要在服务器上全部实现,因为这是确保流程完整性并控制流程以避免竞争条件的唯一方法。