我给你一个 真实项目级的例子:
用 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()
}
十、什么时候用哪种方案
| 方案 | 推荐度 | 场景 |
|---|---|---|
| 原子 UPDATE | ⭐⭐⭐⭐⭐ | 秒杀、库存 |
| 事务 + FOR UPDATE | ⭐⭐⭐ | 复杂业务 |
| Redis + MQ | ⭐⭐⭐⭐⭐⭐ | 超高并发 |
💡 我再告诉你一个 90% 后端不知道的优化技巧:
MySQL 秒杀最强写法其实是:
UPDATE goods
SET stock = stock - 1
WHERE id = 1 AND stock > 0
LIMIT 1;
原因:
防止误更新
锁范围更小