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

如何解决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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 03:11:01