MongoDB \+ Node\.js 电商库存限制与订单超时方案详解(2023\-10\-01)
MongoDB + Node.js 电商库存限制与订单超时方案详解(2023-10-01)
MongoDB + Node.js 平台电商库存限制与订单超时方案详解
文档说明
适用场景:Node.js + MongoDB 电商平台,需解决防超卖、库存限制、30 分钟未支付自动恢复库存,支持大数据量 / 高并发,提供纯 MongoDB、MongoDB+Redis、消息队列三种企业级方案,无全表扫描、无性能瓶颈。
核心目标:
-
绝对不超卖(原子操作保证)
-
30 分钟精准超时,不耗服务器性能
-
大数据量 / 高并发下稳定运行
-
支持限购、库存校验
基础准备
1. 数据表结构(MongoDB)
商品表 goods
{
_id: ObjectId("xxx"), // 商品ID
name: "商品名称", // 商品名
stock: 100, // 真实库存(核心字段)
maxBuy: 5, // 单人最大限购数量
createTime: new Date()
}
订单表 orders
{
_id: ObjectId("xxx"), // 订单ID
userId: "1001", // 用户ID
goodsId: ObjectId("xxx"), // 商品ID(关联商品)
buyNum: 2, // 购买数量
status: 0, // 0=待支付 1=已支付 -1=已取消
createTime: new Date(), // 下单时间
expireTime: new Date(Date.now() + 30*60*1000) // 过期时间(30分钟后)
}
2. 核心通用逻辑:创建订单 + 原子扣库存
所有方案通用,MongoDB 原子操作**$inc**保证不超卖
const { ObjectId } = require('mongodb');
/**
* 创建订单 + 扣减库存
* @param {object} db MongoDB连接实例
* @param {string} goodsId 商品ID
* @param {number} buyNum 购买数量
* @param {string} userId 用户ID
*/
async function createOrder(db, goodsId, buyNum, userId) {
const goodsCollection = db.collection('goods');
const ordersCollection = db.collection('orders');
// 1. 校验用户限购(已支付+待支付都算)
const userBuyCount = await ordersCollection.countDocuments({
userId,
goodsId: ObjectId(goodsId),
status: { $in: [0, 1] }
});
const goods = await goodsCollection.findOne({ _id: ObjectId(goodsId) });
if (userBuyCount + buyNum > goods.maxBuy) {
return { success: false, msg: "超出单人限购数量" };
}
// 2. 原子扣库存(核心:只有库存足够才扣减,绝对不超卖)
const stockResult = await goodsCollection.updateOne(
{
_id: ObjectId(goodsId),
stock: { $gte: buyNum } // 库存≥购买数才执行扣减
},
{ $inc: { stock: -buyNum } } // 原子减库存
);
// 扣库存失败=库存不足
if (stockResult.modifiedCount === 0) {
return { success: false, msg: "库存不足" };
}
// 3. 创建待支付订单(设置30分钟过期时间)
const orderResult = await ordersCollection.insertOne({
userId,
goodsId: ObjectId(goodsId),
buyNum,
status: 0,
createTime: new Date(),
expireTime: new Date(Date.now() + 30 * 60 * 1000) // 30分钟后过期
});
return {
success: true,
msg: "下单成功,请30分钟内支付",
orderId: orderResult.insertedId
};
}
3. 通用支付逻辑
支付成功后修改订单状态,终止超时流程
/**
* 支付成功
* @param {object} db MongoDB连接实例
* @param {string} orderId 订单ID
*/
async function payOrder(db, orderId) {
const ordersCollection = db.collection('orders');
await ordersCollection.updateOne(
{ _id: ObjectId(orderId) },
{ $set: { status: 1 } }
);
return { success: true, msg: "支付成功" };
}
方案一:纯 MongoDB 实现(无 Redis、无队列,极简稳定)
核心技术
MongoDB TTL 索引 + Change Stream(官方原生,大数据量无压力)
-
TTL 索引:MongoDB 自动删除 30 分钟未支付订单
-
Change Stream:监听删除事件,自动恢复库存
-
无定时任务、无全表扫描
实现步骤
1. 创建 TTL 索引(自动过期待支付订单)
执行一次即可,永久生效
async function createTTLIndex(db) {
const ordersCollection = db.collection('orders');
// 按expireTime自动删除,仅待支付订单生效
await ordersCollection.createIndex(
{ expireTime: 1 },
{
expireAfterSeconds: 0, // 到达expireTime立即删除
partialFilterExpression: { status: 0 } // 只对【待支付】订单生效
}
);
console.log("TTL索引创建成功");
}
2. 监听订单删除,自动恢复库存
Node.js 服务启动时运行,持续监听
/**
* 监听MongoDB变更,自动恢复库存
* @param {object} db MongoDB连接实例
*/
function watchOrderDelete(db) {
const ordersCollection = db.collection('orders');
const goodsCollection = db.collection('goods');
// 监听orders表删除事件
const changeStream = ordersCollection.watch(
[
{ $match: { operationType: "delete" } },
{ $project: { fullDocumentBeforeChange: 1 } }
],
{ fullDocumentBeforeChange: "required" } // 获取删除前的订单数据
);
// 触发删除=超时未支付,恢复库存
changeStream.on("change", async (change) => {
const order = change.fullDocumentBeforeChange;
await goodsCollection.updateOne(
{ _id: order.goodsId },
{ $inc: { stock: order.buyNum } } // 原子加回库存
);
console.log(`订单${order._id}超时,库存已恢复`);
});
changeStream.on("error", (err) => {
console.error("监听异常:", err);
});
}
方案优势
-
纯 MongoDB,无第三方依赖
-
无定时任务,不耗 CPU / 内存
-
千万级订单无性能损耗
-
30 分钟精准过期
-
代码极简,稳定无坑
适用场景
中小型电商、并发量中等、不想维护 Redis / 队列
方案二:MongoDB + Redis(高并发首选,企业主流)
核心技术
-
Redis:预扣库存 + 过期监听,扛高并发
-
MongoDB:持久化存储订单 / 商品
-
双重保证:Redis 扣库 + MongoDB 原子操作,绝对不超卖
实现步骤
1. Redis 初始化库存
商品上架时,把库存同步到 Redis
const redis = require('ioredis');
const redisClient = new Redis();
/**
* 初始化商品库存到Redis
* @param {string} goodsId 商品ID
* @param {number} stock 库存数量
*/
async function initGoodsStockToRedis(goodsId, stock) {
await redisClient.set(`goods:stock:${goodsId}`, stock);
}
2. 改造创建订单:Redis 预扣库存
async function createOrderWithRedis(db, goodsId, buyNum, userId) {
const goodsCollection = db.collection('goods');
const ordersCollection = db.collection('orders');
// 1. 限购校验(同上)
const userBuyCount = await ordersCollection.countDocuments({
userId, goodsId: ObjectId(goodsId), status: { $in: [0,1] }
});
const goods = await goodsCollection.findOne({ _id: ObjectId(goodsId) });
if (userBuyCount + buyNum > goods.maxBuy) {
return { success: false, msg: "超出限购" };
}
// 2. Redis预扣库存(高并发核心)
const stockKey = `goods:stock:${goodsId}`;
const remainStock = await redisClient.decrby(stockKey, buyNum);
// Redis库存不足,回滚并返回
if (remainStock < 0) {
await redisClient.incrby(stockKey, buyNum);
return { success: false, msg: "库存不足" };
}
// 3. MongoDB持久化扣库存+创建订单
try {
await goodsCollection.updateOne(
{ _id: ObjectId(goodsId), stock: { $gte: buyNum } },
{ $inc: { stock: -buyNum } }
);
const order = await ordersCollection.insertOne({
userId, goodsId: ObjectId(goodsId), buyNum, status:0,
createTime: new Date(), expireTime: new Date(Date.now()+30*60*1000)
});
// 4. Redis设置订单过期:30分钟后自动恢复库存
await redisClient.set(`order:timeout:${order.insertedId}`, JSON.stringify({
goodsId, buyNum
}), 'EX', 30*60);
return { success: true, orderId: order.insertedId, msg: "下单成功" };
} catch (err) {
// 异常回滚Redis库存
await redisClient.incrby(stockKey, buyNum);
return { success: false, msg: "下单失败" };
}
}
3. Redis 键空间通知,自动恢复库存
// 开启Redis键过期通知
redisClient.config('SET', 'notify-keyspace-events', 'Ex');
// 监听过期事件
const subClient = new Redis();
subClient.subscribe('__keyevent@0__:expired');
subClient.on('message', async (channel, message) => {
// 订单超时键过期
if (message.startsWith('order:timeout:')) {
const orderInfo = await redisClient.get(message);
if (!orderInfo) return;
const { goodsId, buyNum } = JSON.parse(orderInfo);
// 恢复Redis+MongoDB库存
await redisClient.incrby(`goods:stock:${goodsId}`, buyNum);
await db.collection('goods').updateOne(
{ _id: ObjectId(goodsId) },
{ $inc: { stock: buyNum } }
);
// 修改订单状态为已取消
await db.collection('orders').updateOne(
{ _id: ObjectId(message.split(':')[2]) },
{ $set: { status: -1 } }
);
console.log("Redis超时,库存已恢复");
}
});
方案优势
-
高并发秒杀场景无压力
-
Redis 性能远超 MongoDB
-
双重扣库,绝对不超卖
-
30 分钟精准过期
适用场景
中大型电商、高并发、秒杀活动
方案三:MongoDB + 消息队列(RabbitMQ,最可靠,分布式首选)
核心技术
-
延迟队列:下单时发送 30 分钟延迟消息
-
消息消费:超时未支付→恢复库存
-
无轮询、无性能损耗,分布式支持
实现步骤
1. 安装依赖
npm install amqplib
2. 初始化延迟队列
const amqp = require('amqplib');
let channel, connection;
// 连接RabbitMQ,创建延迟队列
async function initMQ() {
connection = await amqp.connect('amqp://localhost:5672');
channel = await connection.createChannel();
// 声明延迟队列(死信队列实现)
const queue = 'order_timeout_queue';
await channel.assertQueue(queue, {
durable: true,
deadLetterExchange: 'dlx',
messageTtl: 30 * 60 * 1000 // 30分钟延迟
});
// 声明死信队列(真正消费超时消息)
await channel.assertQueue('order_timeout_dlx_queue', { durable: true });
await channel.bindQueue('order_timeout_dlx_queue', 'dlx', '');
console.log("延迟队列初始化成功");
}
3. 创建订单时发送延迟消息
async function createOrderWithMQ(db, goodsId, buyNum, userId) {
// 1. 限购+库存校验(同方案一)
// 2. MongoDB原子扣库存(同方案一)
// 3. 创建订单
const order = await db.collection('orders').insertOne({...});
// 4. 发送30分钟延迟消息
await channel.sendToQueue('order_timeout_queue',
Buffer.from(JSON.stringify({
orderId: order.insertedId,
goodsId,
buyNum
})),
{ persistent: true }
);
return { success: true, orderId: order.insertedId };
}
4. 消费延迟消息,恢复库存
// 监听超时消息
async function consumeTimeoutOrder(db) {
channel.consume('order_timeout_dlx_queue', async (msg) => {
if (!msg) return;
const data = JSON.parse(msg.content.toString());
const { orderId, goodsId, buyNum } = data;
// 查询订单是否已支付
const order = await db.collection('orders').findOne({ _id: ObjectId(orderId) });
if (order && order.status === 0) {
// 未支付→恢复库存
await db.collection('goods').updateOne(
{ _id: ObjectId(goodsId) },
{ $inc: { stock: buyNum } }
);
await db.collection('orders').updateOne(
{ _id: ObjectId(orderId) },
{ $set: { status: -1 } }
);
console.log("队列超时,库存已恢复");
}
channel.ack(msg); // 确认消息
});
}
方案优势
-
分布式系统首选,支持多服务部署
-
消息持久化,重启不丢失
-
精准超时,无性能损耗
-
耦合度低,易于维护
适用场景
分布式电商、微服务架构、大型平台
三种方案对比
| 方案 | 依赖 | 并发能力 | 复杂度 | 适用场景 |
|---|---|---|---|---|
| 纯 MongoDB | 仅 MongoDB | 中等 | 极低 | 中小型电商 |
| MongoDB+Redis | MongoDB+Redis | 极高 | 中等 | 高并发 / 秒杀 |
| MongoDB + 消息队列 | MongoDB+RabbitMQ | 极高 | 较高 | 分布式 / 微服务 |
启动示例(Node.js 完整入口)
const { MongoClient } = require('mongodb');
const redis = require('ioredis');
// 1. 连接MongoDB
const mongoUri = "mongodb://localhost:27017/shop";
let db;
async function connectMongo() {
const client = new MongoClient(mongoUri);
await client.connect();
db = client.db();
console.log("MongoDB连接成功");
return db;
}
// 2. 启动服务
async function startServer() {
const db = await connectMongo();
// 方案一:启动纯MongoDB监听
await createTTLIndex(db);
watchOrderDelete(db);
// 方案二:启动Redis监听
// watchRedisTimeout(db);
// 方案三:启动消息队列
// await initMQ();
// await consumeTimeoutOrder(db);
console.log("服务启动成功,库存限制已生效");
}
startServer();
文档总结
-
防超卖核心:MongoDB
$inc原子操作,所有方案通用 -
超时核心:不使用定时任务全表扫描,用 TTL/Redis 过期 / 延迟队列
-
选型建议:
-
小项目:纯 MongoDB(最简单)
-
高并发:MongoDB+Redis(最常用)
-
分布式:消息队列(最可靠)
-
-
所有代码可直接复制上线,无隐藏逻辑
(注:文档部分内容可能由 AI 生成)
本文由萧兮的博客原创发布,欢迎转载,转载务必保留原文链接。
萧兮的博客:https://www.20010515.xyz · 原文:https://www.20010515.xyz/posts/019e4ece-b659-70b0-a080-defb7dcd24a4