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

Express.js+MongoDB高流量场景下解决库存超卖(Race Condition)问题

问题描述

我用Express.js和MongoDB搭建了带闪购功能的电商网站,闪购时段每分钟有5000名用户访问,目前遇到高流量下库存负数(超卖)的问题,推测是竞态条件导致:当库存仅剩1件时,多名用户同时查询到库存充足,完成交易后库存被多次扣减,最终出现负数(比如1-5=-4)。

最初的代码:

const product = await this.productService.findOne(slug)

if( !product )
  return res.status(404).json({ success: false, message: 'Not Found' })

if( product.available <= 0 )
  return res.json({ success: false, message: 'Out of Stock' })

const newTransaction = await this.transactionService.create(user, product)

await this.productService.update(product.id, { available: product.available - 1 })

return res.json({ success: true, data: newTransaction })

之后添加了随机延迟临时缓解,当前代码:

await sleep(200, 500) // 自定义随机延迟函数,参数为最小/最大毫秒数

const product = await this.productService.findOne(slug)

if( !product )
  return res.status(404).json({ success: false, message: 'Not Found' })

if( product.available <= 0 )
  return res.json({ success: false, message: 'Out of Stock' })

const newTransaction = await this.transactionService.create(user, product)

await this.productService.update(product.id, { available: product.available - 1 })

return res.json({ success: true, data: newTransaction })

当前代码能减少80-90%的库存负数,但极高流量下仍会超卖,且本地无法复现(需要同时发2000-5000次请求),求可行解决方案及高并发模拟工具。

可行解决方案

1. 使用MongoDB原子更新操作

核心是把「查询库存+扣减库存」合并为一个原子操作,避免竞态。直接在update时判断库存是否大于0,同时完成扣减:

// 替换原有的查询+更新逻辑
const updatedProduct = await this.productService.updateOne(
  { slug, available: { $gt: 0 } }, // 查询条件:商品存在且库存>0
  { $inc: { available: -1 } } // 原子扣减库存
)

// 检查是否有文档被修改
if (updatedProduct.modifiedCount === 0) {
  return res.json({ success: false, message: 'Out of Stock' })
}

// 库存扣减成功后再创建交易记录
const newTransaction = await this.transactionService.create(user, updatedProduct)

return res.json({ success: true, data: newTransaction })

updateOne是原子操作,MongoDB会保证同一时间只有一个请求能满足条件并完成扣减,彻底避免竞态。

2. 乐观锁(版本号机制)

给商品文档加一个version字段,每次更新时校验版本号,只有版本匹配才允许更新:

// 1. 查询商品及版本号
const product = await this.productService.findOne(slug)
if (!product) return res.status(404).json({ success: false, message: 'Not Found' })
if (product.available <= 0) return res.json({ success: false, message: 'Out of Stock' })

// 2. 尝试原子更新:版本号匹配且库存>0时扣减
const updatedProduct = await this.productService.updateOne(
  { _id: product._id, version: product.version, available: { $gt: 0 } },
  { $inc: { available: -1 }, $set: { version: product.version + 1 } }
)

if (updatedProduct.modifiedCount === 0) {
  // 版本不匹配或库存不足,重试或返回失败
  return res.json({ success: false, message: 'Transaction failed, please try again' })
}

// 3. 创建交易记录
const newTransaction = await this.transactionService.create(user, product)
return res.json({ success: true, data: newTransaction })

如果并发请求导致版本不匹配,需要前端引导用户重试,适合对数据一致性要求高的场景。

3. 悲观锁(MongoDB事务+findOneAndUpdate)

用MongoDB的事务结合findOneAndUpdate的排他锁,锁定商品文档直到操作完成:

const session = await this.productService.startSession()
session.startTransaction()

try {
  // 查询并锁定商品,直到事务提交
  const product = await this.productService.findOneAndUpdate(
    { slug, available: { $gt: 0 } },
    { $inc: { available: -1 } },
    { session, returnOriginal: false } // 返回更新后的文档
  )

  if (!product) {
    await session.abortTransaction()
    return res.json({ success: false, message: 'Out of Stock' })
  }

  // 在同一个事务中创建交易记录
  await this.transactionService.create(user, product, { session })

  await session.commitTransaction()
  return res.json({ success: true, data: product })
} catch (err) {
  await session.abortTransaction()
  return res.status(500).json({ success: false, message: 'Server Error' })
} finally {
  session.endSession()
}

注意MongoDB事务需要副本集或分片集群,单节点不支持,适合需要跨文档原子操作的场景。

4. 消息队列削峰

把下单请求放入消息队列(比如Redis Queue、BullMQ),后台用单进程或有限进程消费,串行处理库存扣减:

// 前端请求接口:将下单请求加入队列
const job = await this.queue.add('place-order', { userId: user.id, productSlug: slug })

return res.json({ success: true, message: 'Order processing, please wait' })

// 后台消费队列
this.queue.process('place-order', async (job) => {
  const { userId, productSlug } = job.data
  const user = await this.userService.findOne(userId)
  const product = await this.productService.findOne(productSlug)

  if (!product || product.available <= 0) {
    await this.notificationService.send(user, 'Order failed: Out of Stock')
    return
  }

  await this.productService.updateOne({ _id: product._id }, { $inc: { available: -1 } })
  const transaction = await this.transactionService.create(user, product)
  await this.notificationService.send(user, 'Order success', transaction)
})

这种方式能彻底解决并发问题,但会增加系统复杂度,适合流量极大的闪购场景。

高并发请求模拟工具

1. k6

轻量级的JavaScript压测工具,适合模拟高并发请求:
编写压测脚本load-test.js:

import http from 'k6/http'
import { check } from 'k6'

export const options = {
  vus: 5000, // 虚拟用户数
  duration: '1m', // 持续时间
}

export default function () {
  const res = http.post('http://your-api.com/api/order', JSON.stringify({
    slug: 'flash-sale-product',
    userId: 'test-user-id'
  }), {
    headers: { 'Content-Type': 'application/json' }
  })
  check(res, { 'status is 200': (r) => r.status === 200 })
}

运行命令:k6 run load-test.js

2. Artillery

基于Node.js的压测工具,配置简单:
编写config.yml:

config:
  target: 'http://your-api.com'
  phases:
    - duration: 60
      arrivalRate: 5000 # 每分钟5000请求
scenarios:
  - flow:
      - post:
          url: '/api/order'
          json:
            slug: 'flash-sale-product'
            userId: 'test-user-id'

运行命令:artillery run config.yml

3. Locust

Python编写的分布式压测工具,支持Web UI监控:
编写locustfile.py:

from locust import HttpUser, task, between

class OrderUser(HttpUser):
    wait_time = between(0, 0) # 无延迟,模拟并发

    @task
    def place_order(self):
        self.client.post("/api/order", json={
            "slug": "flash-sale-product",
            "userId": "test-user-id"
        })

运行命令:locust -f locustfile.py,然后访问http://localhost:8089设置并发数和请求速率。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.14 04:45:00