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
相关产品推荐
相关产品推荐

