mysql + nest.js 加锁 搞并发问题

我给你一个 真实项目级的例子

NestJS + MySQL 原生 SQL 实现 秒杀库存扣减 ,并解决 高并发超卖问题

这个例子是很多电商系统的核心逻辑。


一、数据库设计

商品表

sql 复制代码
CREATE TABLE goods (
  id INT PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(100),
  stock INT NOT NULL,
  price DECIMAL(10,2),
  create_time DATETIME DEFAULT CURRENT_TIMESTAMP
);

订单表

sql 复制代码
CREATE TABLE orders (
  id INT PRIMARY KEY AUTO_INCREMENT,
  user_id INT,
  goods_id INT,
  create_time DATETIME DEFAULT CURRENT_TIMESTAMP
);

初始化库存

sql 复制代码
INSERT INTO goods (name, stock, price)
VALUES ('iphone 15', 10, 8999);

二、问题:高并发超卖

如果代码这样写:

vbnet 复制代码
const goods = await db.query('SELECT * FROM goods WHERE id = ?', [id])

if (goods.stock > 0) {
  await db.query('UPDATE goods SET stock = stock - 1 WHERE id = ?', [id])
}

1000 个请求会出现:

复制代码
库存10
卖出去100

因为:

复制代码
读取库存不是原子操作

三、正确方案一(推荐):原子 SQL

最推荐的写法:

ini 复制代码
UPDATE goods
SET stock = stock - 1
WHERE id = ? AND stock > 0;

优点:

sql 复制代码
单条 SQL
原子操作
不会超卖
性能极高

四、NestJS 原生 SQL 实现

使用 MySQL 连接池。

安装:

复制代码
npm install mysql2

数据库服务

typescript 复制代码
import { Injectable } from '@nestjs/common'
import * as mysql from 'mysql2/promise'

@Injectable()
export class MysqlService {

  pool = mysql.createPool({
    host: 'localhost',
    user: 'root',
    password: '123456',
    database: 'test',
    connectionLimit: 10
  })

  async query(sql: string, params?: any[]) {
    const [rows] = await this.pool.execute(sql, params)
    return rows
  }

}

五、秒杀 Service

typescript 复制代码
import { Injectable } from '@nestjs/common'
import { MysqlService } from './mysql.service'

@Injectable()
export class SeckillService {

  constructor(private db: MysqlService) {}

  async buy(userId: number, goodsId: number) {

    // 1 扣库存(原子SQL)
    const result: any = await this.db.query(
      `UPDATE goods 
       SET stock = stock - 1 
       WHERE id = ? AND stock > 0`,
      [goodsId]
    )

    if (result.affectedRows === 0) {
      return {
        message: '库存不足'
      }
    }

    // 2 创建订单
    await this.db.query(
      `INSERT INTO orders (user_id, goods_id) VALUES (?, ?)`,
      [userId, goodsId]
    )

    return {
      message: '秒杀成功'
    }

  }

}

六、Controller

typescript 复制代码
import { Controller, Post, Body } from '@nestjs/common'
import { SeckillService } from './seckill.service'

@Controller('seckill')
export class SeckillController {

  constructor(private seckillService: SeckillService) {}

  @Post('buy')
  async buy(@Body() body) {

    const { userId, goodsId } = body

    return this.seckillService.buy(userId, goodsId)

  }

}

七、执行流程(高并发)

1000个请求:

sql 复制代码
UPDATE goods SET stock = stock - 1 WHERE id=1 AND stock>0

MySQL 内部会保证:

复制代码
库存10

前10个请求成功
990个失败

不会出现:

ini 复制代码
stock = -1

八、真实大厂架构(完整版)

真正秒杀系统通常是这样:

scss 复制代码
用户请求
   ↓
Nginx
   ↓
Redis 判断库存
   ↓
消息队列 (Kafka / RabbitMQ)
   ↓
NestJS 消费
   ↓
MySQL 扣库存

作用:

复制代码
Redis      防止打爆数据库
MQ         削峰
MySQL      最终一致

九、如果必须用 MySQL Lock

有些复杂业务必须用:

sql 复制代码
SELECT * FROM goods WHERE id = 1 FOR UPDATE;

NestJS 示例:

csharp 复制代码
const conn = await this.db.pool.getConnection()

try {

  await conn.beginTransaction()

  const [goods]: any = await conn.query(
    'SELECT * FROM goods WHERE id = ? FOR UPDATE',
    [goodsId]
  )

  if (goods.length === 0 || goods[0].stock <= 0) {
    await conn.rollback()
    return { message: '库存不足' }
  }

  await conn.query(
    'UPDATE goods SET stock = stock - 1 WHERE id = ?',
    [goodsId]
  )

  await conn.query(
    'INSERT INTO orders (user_id, goods_id) VALUES (?, ?)',
    [userId, goodsId]
  )

  await conn.commit()

  return { message: '购买成功' }

} catch (err) {

  await conn.rollback()
  throw err

} finally {

  conn.release()

}

十、什么时候用哪种方案

方案 推荐度 场景
原子 UPDATE ⭐⭐⭐⭐⭐ 秒杀、库存
事务 + FOR UPDATE ⭐⭐⭐ 复杂业务
Redis + MQ ⭐⭐⭐⭐⭐⭐ 超高并发

💡 我再告诉你一个 90% 后端不知道的优化技巧:

MySQL 秒杀最强写法其实是:

ini 复制代码
UPDATE goods 
SET stock = stock - 1 
WHERE id = 1 AND stock > 0
LIMIT 1;

原因:

复制代码
防止误更新
锁范围更小
相关推荐
烂蜻蜓8 小时前
Flask入门教程(十二):表单处理——从基础到Flask-WTF完整实践
后端·python·flask
2601_9620559715 小时前
跟据spring boot版本,查看对应的tomcat,并查看可支持的tomcat的版本范围
spring boot·后端·tomcat
Lost of 程序猿16 小时前
ASP.NET Core API 幂等性设计深度实战:从一次重复申领事故说起
后端·asp.net
QQ_216962909618 小时前
【源码编号:project79475】SpringBoot校内二手交易平台:商品发布、分类检索、留言交流、订单管理全流程实战
java·spring boot·后端
郑州光合科技余经理19 小时前
本地生活服务系统:模块边界与结算字段怎么拆
java·开发语言·前端·后端·系统架构·uni-app·php
IT_陈寒19 小时前
Vue的嵌套组件竟然吃掉了我的事件?
前端·人工智能·后端
大辉狼_音频架构20 小时前
进阶:从源码编译 SOF 固件与 topology
后端
大辉狼_音频架构20 小时前
上板:让 SOF 在 FRDM-i.MX8MP 上跑起来
后端
大辉狼_音频架构20 小时前
FRDM-IMX8MP UUU 烧录 eMMC 指南
后端
运行时异常21 小时前
【WMS 仓储系统集成 AI Agent 实战】第 3 讲:Spring Security 6 + JWT——addFilterBefore 一词之差,全站 401
java·后端