You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

bcrypt加密计时竞态条件:MongoDB注册重复提交致重复创建文档

解决并发注册时重复创建用户的问题

这是个非常典型的并发场景问题,我之前做用户系统时也踩过类似的坑,给你梳理几个层层递进的解决方案,从根本上搞定它:


1. 先做用户存在性检查,再执行密码哈希

你之前尝试在哈希后检查的顺序是反的!正确的流程应该是:

  • 收到注册请求后第一时间查询数据库,确认用户名是否已经存在
  • 只有确认不存在时,再执行bcrypt.hash和后续的创建操作

但这一步只能解决大部分普通场景,极端情况下两个请求同时到达,都查询到用户名不存在,然后同时进入哈希流程,还是会出现重复创建的问题,所以还需要下面的保障。

2. 给MongoDB的用户名字段加唯一索引(最核心的防线)

不管业务层怎么处理,数据库层面的约束才是最后一道保险。在MongoDB中给username字段创建唯一索引,这样即使两个请求都走到写入步骤,数据库会直接抛出重复键错误,不会生成重复文档。

执行以下命令创建索引:

db.users.createIndex({ username: 1 }, { unique: true })

然后在代码里捕获这个错误(错误码11000),返回给用户友好提示:

try {
  // 创建用户的代码
  await user.save();
} catch (err) {
  if (err.code === 11000) {
    return res.status(400).send("该用户名已被注册");
  }
  // 处理其他错误
}

3. 加锁机制,避免并发请求同时处理同一用户名

如果你的服务是单实例部署,可以用内存锁来标记正在处理的用户名;如果是多实例,就用分布式锁(比如基于Redis或MongoDB自身的原子操作)。

单实例内存锁示例

// 用Map存储正在处理的用户名
const processingUsers = new Map();

async function register(req, res) {
  const { username, password } = req.body;
  
  // 检查是否有其他请求正在处理该用户名
  if (processingUsers.has(username)) {
    return res.status(400).send("该用户名正在注册,请稍后重试");
  }
  
  processingUsers.set(username, true);
  try {
    // 先查用户是否存在
    const existingUser = await User.findOne({ username });
    if (existingUser) {
      return res.status(400).send("用户名已存在");
    }
    
    // 哈希密码
    const hashedPassword = await bcrypt.hash(password, 10);
    
    // 创建用户
    const user = new User({ username, password: hashedPassword });
    await user.save();
    
    res.status(201).send("注册成功");
  } catch (err) {
    if (err.code === 11000) {
      res.status(400).send("用户名已存在");
    } else {
      res.status(500).send("服务器错误");
    }
  } finally {
    // 不管成功失败,都要释放锁
    processingUsers.delete(username);
  }
}

多实例分布式锁(基于MongoDB原子操作)

async function register(req, res) {
  const { username, password } = req.body;
  
  try {
    // 原子操作:尝试给不存在的用户添加"正在注册"标记,只有成功的请求能继续
    const result = await User.findOneAndUpdate(
      { username, isRegistering: { $exists: false } },
      { $set: { isRegistering: true } },
      { upsert: false, new: true }
    );
    
    if (!result) {
      return res.status(400).send("用户名已存在或正在注册,请稍后重试");
    }
    
    // 哈希密码
    const hashedPassword = await bcrypt.hash(password, 10);
    
    // 完成注册,移除标记并设置密码
    await User.findOneAndUpdate(
      { username },
      { $set: { password: hashedPassword }, $unset: { isRegistering: "" } }
    );
    
    res.status(201).send("注册成功");
  } catch (err) {
    if (err.code === 11000) {
      res.status(400).send("用户名已存在");
    } else {
      // 出错时清理标记,避免死锁
      await User.updateOne({ username }, { $unset: { isRegistering: "" } });
      res.status(500).send("服务器错误");
    }
  }
}

4. 前端限制重复提交(辅助手段)

从源头减少重复请求的可能:用户点击注册按钮后,立即禁用按钮,直到请求完成(成功或失败)再恢复。示例代码:

const registerBtn = document.getElementById('register-btn');
registerBtn.addEventListener('click', async () => {
  registerBtn.disabled = true;
  try {
    const response = await fetch('/api/register', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ username: 'xxx', password: 'xxx' })
    });
    const data = await response.json();
    alert(data.message);
  } catch (err) {
    alert('注册失败,请重试');
  } finally {
    registerBtn.disabled = false;
  }
});

内容的提问来源于stack exchange,提问作者simon

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.19 09:10:37