创建锁定,以便两个用户不会使用相同的用户名

时间:2015-06-27 05:55:39

标签: javascript node.js concurrency

我正在使用带有Node.js的Express服务器,我担心并发性并阻止用户同时使用相同的用户名创建帐户。从我的underatanding,尽管主Node.js运行时是单线程的,一个函数仍然可能开始而另一个尚未完成。 一种选择是在像Redis这样的外部源上创建锁,并使用它来控制并发 - 但我的问题是 - 是否有一种可靠的方法来在本机JavaScript中控制它?例如,某个级别的嵌套函数基本上是为了保证新请求在另一个完成之前不会发起某个调用(我对此表示怀疑)。

更具体一点 - 我正在使用MongoDB和Mongoose。使用Mongoose模式在内存中应该有一个唯一的用户列表。那么会发生什么:

(1)新用户注册用户名A

(2)node.js将A与索引的用户列表进行比较,发现A确实是新的用户名

(3)node.js进行数据库调用以将A添加到列表

(4)之前(3)完成 new 用户注册用户名A ... node.js将A与用户名索引进行比较,它看起来是唯一的,因此它会向DB发送一个插入< / p> 问题似乎是真的。我的猜测是解决它的最佳方法是外部锁定。

3 个答案:

答案 0 :(得分:2)

正如您所怀疑的那样,尽管node.js是单线程的,但只要您的代码参与任何异步操作(如任何数据库操作),在异步操作期间就有机会开始另一个请求开始处理。因此,您确实可以同时在飞行中获得两个(或更多)冲突操作。

这里有一些主要的方法。

  1. 数据库。让创建和存储userID的数据库强制执行唯一性。
  2. 序列化。序列化所有可能存在冲突的操作。
  3. Inflight Control。保留您自己的“机上”用户名的内存列表。
  4. 我将对每一个进行更多描述。

    数据库。由于数据库本身是用户名的最终记录,因此有时最简单的方法就是让它告诉尝试创建无法创建该用户名的用户名的第二个请求,因为它已经存在。当你这样做的时候,你只需让win中的第一个请求和任何后续的请求都会出错,因为用户名已经存在。虽然您可能不会将此作为唯一的保护,因为它总是更好地预先飞行并让用户在提交已经使用的新用户名之前知道它,它可以成为并发情况下的后盾。

    由于您怀疑并发问题,您在步骤1),2)和3)中描述的方法是执行数据库操作的绝佳方法。如果可以在您的步骤之间更改数据库,这将使您的逻辑无效或导致您执行错误的数据库操作,则逻辑必须更改。

    相反,您希望创建一个成功或失败的原子操作。因此,您希望在数据库中创建新的建议用户ID。如果它成功创建了一个新的userID,那么你很好,它必须是唯一的,现在它已经代表这个请求创建了。那里没有并发问题。如果userID已存在,则创建操作应该失败。您不想测试userID,然后决定创建它,因为这会创建并发窗口。将数据库操作设计为完全原子的。一个成功或失败的数据库操作。然后你让数据库处理任何瞬时锁定并让它做出艰苦的工作。

    序列化操作。您可以实现自己的流量流,一次只允许一个请求创建新用户。这将使任何其他请求等待正在进行的请求完成。从可扩展性和吞吐量的角度来看,这不太理想,但它在概念上很简单。

    手动机上控制。假设您没有运行群集服务器,因此在您的数据库中只有一个Web服务器创建帐户,您可以为流程中的任何用户名保留内存数据结构被创造。当任何数据库请求出现以创建用户名时,您可以首先检查内存中的数据结构,以查看是否正在创建此用户名的请求。如果是,则拒绝新请求。

    由于用户名的权威来源是数据库,并且它必须拥有自己的并发控件,我的首选是让数据库告诉第二个请求创建用户名yyy用户名已存在并让错误渗透回到最终用户。这种方法没有边缘情况和最佳性能。您仍然可以实施飞行前检查,以便为用户提供有关已存在的用户名的最佳和最直接的反馈,但在并发冲突的情况下使用数据库作为您的支持。

    请注意,如果以这种方式实现它,则必须确保服务器和客户端中的错误处理路径都是干净的。服务器必须不让部分创建一个帐户然后出错并使其处于错误状态,客户端必须知道如何正确显示该错误情况以及离开用户的状态。

答案 1 :(得分:2)

解决此问题的最简单方法是在相应的mongo集合上使用unique index。这将阻止任何会导致重复值的插入或更新(请注意,这是由mongodb服务器强制执行的,而不是mongoose)。

我不熟悉mongoose,但是当你告诉它一个属性是唯一的时,它会创建相应的唯一索引。

答案 2 :(得分:2)

您可以创建一个保留名称或待定名称列表。

var pendingNames = [];

当用户A想要使用用户名“蝙蝠侠”注册时,您将首先检查名称本身是否被拍摄。

如果未采用该名称,您将检查待处理名称列表中的名称。

var name = "Batman";
if(pendingNames.indexOf(name.toLowerCase()) !== -1) {
    //abort the registration
}

// if its not in the pending list, simply add it to it
pendingNames.push(name.toLowerCase());

然后在写入成功或失败后,从待处理列表中删除该名称。

还有其他方法,但我发现这是最快速验证名称是否可以注册。