我给你一个 真实项目级的例子:
用 NestJS + MySQL 原生 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 );
订单表
CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT, goods_id INT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );
初始化库存
INSERT INTO goods (name, stock, price) VALUES ('iphone 15', 10, 8999);
二、问题:高并发超卖
如果代码这样写:
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
最推荐的写法:
UPDATE goods SET stock = stock - 1 WHERE id = ? AND stock > 0;
优点:
单条 SQL
原子操作
不会超卖
性能极高
四、NestJS 原生 SQL 实现
使用 MySQL 连接池。
安装:
npm install mysql2
数据库服务
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
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
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个请求:
UPDATE goods SET stock = stock - 1 WHERE id=1 AND stock>0
MySQL 内部会保证:
库存10
前10个请求成功
990个失败
不会出现:
stock = -1
八、真实大厂架构(完整版)
真正秒杀系统通常是这样:
用户请求
↓
Nginx
↓
Redis 判断库存
↓
消息队列 (Kafka / RabbitMQ)
↓
NestJS 消费
↓
MySQL 扣库存
作用:
Redis 防止打爆数据库 MQ 削峰 MySQL 最终一致
九、如果必须用 MySQL Lock
有些复杂业务必须用:
SELECT * FROM goods WHERE id = 1 FOR UPDATE;
NestJS 示例:
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() }
十、什么时候用哪种方案
???? 我再告诉你一个 90% 后端不知道的优化技巧:
MySQL 秒杀最强写法其实是:
UPDATE goods SET stock = stock - 1 WHERE id = 1 AND stock > 0 LIMIT 1;
原因:
防止误更新
锁范围更小
到此这篇关于mysql + nest.js 加锁搞并发问题的实现的文章就介绍到这了,
