如何解决Express.js中MongoDB插入操作的多请求重复注册问题
解决MongoDB并发请求下重复创建用户的问题
背景
我正在学习一篇使用Express.js、MongoDB和JWT搭建安全用户注册/登录API的教程,用到的依赖包括express、bcryptjs、jsonwebtoken和mongoose。
问题描述
当向/api/register接口发送大量并发请求时,服务器会创建多个拥有相同邮箱的用户。
复现代码
服务端代码
// Importing required modules const express = require('express'); const mongoose = require('mongoose'); const bcrypt = require('bcryptjs'); const jwt = require('jsonwebtoken'); // Creating an Express application instance const app = express(); const PORT = 3000; // Connect to MongoDB database mongoose.connect('mongodb://localhost:27017/mydatabase') .then(() => { console.log('Connected to MongoDB'); }) .catch((error) => { console.error('Error connecting to MongoDB:', error); }); // Define a schema for the User collection const userSchema = new mongoose.Schema({ username: String, email: String, password: String }); // Create a User model based on the schema const User = mongoose.model('User', userSchema); // Middleware to parse JSON bodies app.use(express.json()); // Route to register a new user app.post('/api/register', async (req, res) => { try { // Check if the email already exists const existingUser = await User.findOne({email: req.body.email}); if (existingUser) { return res.status(400).json({error: 'Email already exists'}); } // Hash the password const hashedPassword = await bcrypt.hash(req.body.password, 10); // Create a new user const newUser = new User({ username: req.body.username, email: req.body.email, password: hashedPassword }); await newUser.save(); console.log(newUser) res.status(201).json({message: 'User registered successfully'}); } catch (error) { console.log('Internal server error') res.status(500).json({error: 'Internal server error'}); } }); // Start the server app.listen(PORT, () => { console.log(`Server is running on port ${PORT}`); });
客户端代码
(() => { const send = () => fetch('http://127.0.0.1:3000/api/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ "username": "testuser", "email": "test@example.com", "password": "testpassword" }) }).then() for (let i = 0; i < 10; i++) send() })()
输出结果
{ username: 'testuser', email: 'test@example.com', password: '$2a$10$5Jo01CQ797EuMrujH49Of.OmFY4n6yIX.4VgU8FOEhXRmr.CxHnRy', _id: new ObjectId('668cf24a08fa6ed9fbf442d5'), __v: 0 } ...9 more users created
解决方案
原代码的问题在于并发竞态条件:多个请求同时执行User.findOne查询时,都未找到已存在的用户,随后都执行了插入操作。要彻底解决这个问题,必须依赖数据库层面的约束,同时配合代码错误处理:
1. 给邮箱字段添加唯一索引
修改用户Schema,为email字段设置唯一约束,MongoDB会自动创建唯一索引,确保不会插入重复邮箱的文档:
const userSchema = new mongoose.Schema({ username: String, email: { type: String, unique: true }, // 添加唯一约束 password: String });
注意:如果数据库中已存在重复邮箱的旧数据,需要先清理这些数据,否则创建索引会失败。
2. 捕获唯一约束冲突错误
在注册接口的错误处理中,捕获MongoDB的重复键错误(错误码11000),返回对应的提示信息:
app.post('/api/register', async (req, res) => { try { // 哈希密码步骤不变 const hashedPassword = await bcrypt.hash(req.body.password, 10); const newUser = new User({ username: req.body.username, email: req.body.email, password: hashedPassword }); await newUser.save(); console.log(newUser) res.status(201).json({message: 'User registered successfully'}); } catch (error) { // 捕获重复邮箱错误 if (error.code === 11000 && error.keyPattern?.email) { return res.status(400).json({error: 'Email already exists'}); } console.log('Internal server error', error); res.status(500).json({error: 'Internal server error'}); } });
可以移除原有的
User.findOne查询,因为数据库的唯一约束已经能保证不会插入重复数据;保留查询也可以提前返回提示,减少不必要的哈希运算和数据库操作。
可选优化:使用原子操作
如果想进一步优化性能,可以使用findOneAndUpdate配合upsert: false,实现"查询-插入"的原子操作:
app.post('/api/register', async (req, res) => { try { const hashedPassword = await bcrypt.hash(req.body.password, 10); const user = await User.findOneAndUpdate( { email: req.body.email }, { username: req.body.username, email: req.body.email, password: hashedPassword }, { upsert: false, new: true } // upsert设为false,不创建新文档 ); if (!user) { // 不存在则创建新用户 const newUser = new User({ username: req.body.username, email: req.body.email, password: hashedPassword }); await newUser.save(); return res.status(201).json({message: 'User registered successfully'}); } res.status(400).json({error: 'Email already exists'}); } catch (error) { if (error.code === 11000 && error.keyPattern?.email) { return res.status(400).json({error: 'Email already exists'}); } console.log('Internal server error', error); res.status(500).json({error: 'Internal server error'}); } });
但核心还是数据库的唯一约束,这是防止重复数据的最终保障。
内容的提问来源于stack exchange,提问作者Uberbaza
相关产品推荐
相关产品推荐

