「实时竞拍大师」是一套面向抖音电商直播场景设计的高并发竞拍全栈系统, 采用 Redis Lua 原子脚本 + WebSocket 房间广播架构, 实现毫秒级出价响应与多直播间并发隔离。
- 密钥鉴权:通过
x-admin-secret请求头 + 常量时间比较(timingSafeEqual)防护时序攻击。 - 竞拍发布:支持自定义商品名称、图片 URL、起拍价、加价幅度、竞拍时长。
- 竞拍概览:实时展示当前所有竞拍场次(状态前端本地修正,防止 Redis 状态延迟导致的展示偏差)。
- 危险操作:支持主播取消竞拍(广播
auction_cancelled)或提前结束(广播auction_ended)。 - 一键复制:发布成功后一键复制直播间链接,支持新窗口打开
- 竞拍列表:MySQL(静态信息)+ Redis(实时价格/倒计时)并行合并,最多展示 20 条。
- 自动轮询:每 10 秒自动刷新,支持手动刷新。
- 状态筛选:显示「进行中」/「即将开始」标签,倒计时归零后自动过滤。
核心出价逻辑封装在单个不可中断的 Redis Lua 脚本(src/lua/place_bid.lua)中,8 步原子执行:
| 步骤 | 名称 | 说明 |
|---|---|---|
| Step 1 | 限流检查 | INCR 固定窗口计数器,单用户每秒最多 BID_RATE_LIMIT_PER_SECOND 次,TTL 2s |
| Step 2 | 幂等检查 | SET NX PX 原子操作,同一用户/金额 3s 内重复出价被拦截 |
| Step 3 | 读取状态 | HGETALL 一次性获取竞拍全部 10+ 字段 |
| Step 4 | 状态校验 | 仅 status == "active" 接受出价 |
| Step 5 | 时间校验 | 服务端传入 server_timestamp_ms(绝对权威),≥ end_time_ms 则拒绝 |
| Step 6 | 金额校验 | 首单允许以起拍价出价,非首单需 ≥ current_price + min_increment |
| Step 7 | 状态更新 | HSET 更新最高价/得标者;最后 N 秒内出价则自动延长 extend_seconds |
| Step 8 | 排行榜 | ZADD 写入排行榜(同 userId 自动去重),ZREMRANGEBYRANK 裁剪保留 TOP 50 |
关键设计特点:
- 金额统一以**分(整数)**存储和计算,规避浮点精度问题。
- 所有校验-更新操作在单个 Lua 脚本内完成,利用 Redis 单线程模型有效规避并发写冲突。
- 房间隔离:基于 Socket.io Room 机制,每个
auctionId一个房间,多直播间逻辑隔离。 - 事件契约:前后端共用
SOCKET_EVENTS常量,保障接口定义一致性。 - 断线重连:指数退避重试,重连后自动发送
request_state拉取完整状态,前端无感知恢复。 - 广播隔离:
auction_state仅发给请求者(socket.emit),不广播给房间内其他用户,保持安静重连体验。 - 心跳配置:pingInterval 25s + pingTimeout 60s,适应 NAT 网络环境
- 移动端适配:高保真复刻直播间 UI(480×844 比例),含圆角边框和阴影。
- 折叠/展开:BIDDING 状态支持面板收起,切换动画采用 Framer Motion。
- 排行榜面板:左侧实时展示 TOP 5 出价榜,当前用户高亮。
- 出价步进器:快速加减按钮,支持自定义金额输入及超大金额自适应缩放。
- 落锤庆典 Modal:竞拍结束时自动弹出,互斥锁保证不重复触发。
| 层级 | 技术选型 | 版本 | 选型理由 |
|---|---|---|---|
| 后端运行时 | Node.js + TypeScript | ≥20 LTS / 5.4 | 高并发异步 I/O,类型安全 |
| Web 框架 | Express | ^4.19 | 轻量成熟,生态丰富 |
| ORM | Prisma | ^5.14 | 类型安全查询,自动迁移 |
| Redis 客户端 | ioredis | ^5.3 | 支持 Lua 脚本、Pipeline、集群 |
| 缓存/原子锁 | Redis | 7.0-alpine | 单线程模型天然原子性,AOF 持久化 |
| 数据库 | MySQL | 8.0 | 成熟关系型,支持高并发写入 |
| 实时通信 | Socket.io | ^4.8 (server + client) | WebSocket 优先,自动降级 polling |
| 请求校验 | Zod | ^3.23 | 类型推导 + 运行时校验 |
| 前端框架 | React | 18 (Vite) | 高效渲染,Hooks 模式 |
| 样式 | TailwindCSS | 内联样式 | 抖音风格定制,无框架依赖 |
| 动效 | Framer Motion | latest | 高性能声明式动画 |
| 容器化 | Docker Compose | 3.9 | 一键部署 MySQL + Redis |
auction-master/
├── src/ # 后端源码
│ ├── config/
│ │ └── env.ts # 环境变量统一读取与类型化
│ ├── controllers/
│ │ ├── auction.controller.ts # C 端接口:出价、状态、列表、详情
│ │ └── admin.controller.ts # B 端接口:发布、取消、结束竞拍
│ ├── services/
│ │ ├── auction.service.ts # 核心业务:Lua 调用、广播、排行榜解析
│ │ └── admin.service.ts # 管理业务:发布竞拍(含回滚)、取消、结束
│ ├── routes/
│ │ ├── auction.routes.ts # /api/auctions/* 路由注册
│ │ └── admin.routes.ts # /api/admin/* 路由注册(含 adminAuth)
│ ├── socket/
│ │ ├── index.ts # Socket.io 单例初始化与导出
│ │ └── handlers/
│ │ └── auction.handler.ts # 事件处理:join/leave/request_state/disconnect
│ ├── middleware/
│ │ └── adminAuth.ts # 管理后台鉴权(timingSafeEqual)
│ ├── db/
│ │ ├── redis.ts # Redis 连接 + Lua 脚本加载 + EVALSHA 调用
│ │ └── prisma.ts # Prisma 客户端单例
│ ├── lua/
│ │ └── place_bid.lua # 核心出价 8 步原子脚本
│ ├── types/
│ │ └── index.ts # 全局类型:API 响应、Socket Payload、Lua 返回码
│ └── app.ts # Express 应用入口 + bootstrap 启动流程
├── frontend/ # 前端源码(React + Vite + TailwindCSS)
│ ├── src/
│ │ ├── pages/
│ │ │ ├── Lobby.tsx # 拍卖大厅首页(自动轮询)
│ │ │ ├── AuctionRoom.tsx # 竞拍房间(手机模拟器 + 视图状态机)
│ │ │ └── Admin.tsx # 商家后台(密钥登录 + 发布表单 + 危险操作)
│ │ ├── components/
│ │ │ ├── auction/
│ │ │ │ └── AuctionBottomSheet.tsx # 底部竞拍面板(出价/倒计时/结果)
│ │ │ └── live/
│ │ │ ├── LiveBackground.tsx # 视频背景层
│ │ │ ├── LiveHeader.tsx # 顶部主播信息
│ │ │ ├── DanmuFeed.tsx # 弹幕区
│ │ │ ├── InteractionBar.tsx # 右侧互动栏
│ │ │ └── LeaderboardPanel.tsx # 左侧排行榜 TOP 5
│ │ ├── hooks/
│ │ │ └── useAuction.ts # 竞拍状态机(useReducer + Socket + 时钟校准)
│ │ ├── utils/
│ │ │ ├── request.ts # Axios 实例封装(含 401 拦截)
│ │ │ ├── priceFormat.ts # 价格格式化工具
│ │ │ └── haptics.ts # 触觉反馈封装
│ │ ├── constants/
│ │ │ └── socket-events.ts # Socket 事件名常量(与后端同步)
│ │ ├── types/
│ │ │ └── index.ts # 前端类型定义(含 UseAuctionReturn 等)
│ │ └── App.tsx # 路由容器(/ → Lobby, /auction/:id → Room, /admin → Admin)
│ ├── vite.config.ts # Vite 配置(代理 /api 和 /socket.io 到后端)
│ └── .env.example # 前端环境变量示例
├── prisma/
│ └── schema.prisma # 数据库 Schema(5 张核心表 + 2 个 Enum)
├── init-sql/ # MySQL 初始化 SQL(Docker 首次启动自动执行)
├── docs/
│ ├── api_protocol.md # API 协议文档
│ └── benchmark.md # 性能压测报告
├── docker-compose.yml # 容器编排(MySQL 8.0 + Redis 7.0)
├── .env.example # 根目录环境变量示例
└── package.json # 后端项目元信息
| 依赖 | 最低版本 | 说明 |
|---|---|---|
| Node.js | ≥ 20.0.0 LTS | 后端运行时 |
| Docker | ≥ 24.0 | 运行 MySQL + Redis 容器 |
| Docker Compose | ≥ 3.9 | 一键编排数据库服务 |
| npm | ≥ 9.0 | 包管理(随 Node.js 附带) |
# 1. 克隆仓库
git clone <仓库地址>
cd auction-master
# 2. 配置环境变量
cp .env.example .env
# 编辑 .env,将 <CHANGE_ME_*> 占位符替换为真实密码
# 开发环境可用默认值快速启动(请参考 .env.example 配置本地环境变量)
# 3. 启动数据库服务(MySQL + Redis)
docker compose up -d
# 4. 等待 MySQL 初始化完成(约 30 秒)
docker compose logs -f mysql
# 看到 "ready for connections" 后 Ctrl+C 退出
# 5. 安装后端依赖 + 初始化数据库
npm install
npx prisma generate
npx prisma migrate dev
# 6. 启动后端服务(端口 13000)
npm run dev
# 7. 新开终端,安装并启动前端
cd frontend
npm install
npm run dev -- --port 8888
# 8. 访问
# 前端(拍卖大厅):http://localhost:5173
# 竞拍房间示例: http://localhost:5173/auction/1
# 管理后台: http://localhost:5173/admin
# 后端健康检查: http://localhost:13000/health| 变量名 | 必填 | 默认值 | 说明 |
|---|---|---|---|
MYSQL_ROOT_PASSWORD |
✅ | — | MySQL root 密码 |
MYSQL_DATABASE |
— | auction_db |
应用数据库名 |
MYSQL_USER |
— | auction_user |
应用专用账户 |
MYSQL_PASSWORD |
✅ | — | 应用账户密码 |
MYSQL_PORT |
— | 3306 |
MySQL 宿主机映射端口 |
REDIS_PASSWORD |
✅ | — | Redis 认证密码 |
REDIS_MAX_MEMORY |
— | 256mb |
Redis 最大内存限制 |
REDIS_PORT |
— | 6379 |
Redis 宿主机映射端口 |
APP_PORT |
— | 3000 |
后端 HTTP 服务端口 |
NODE_ENV |
— | development |
运行环境 |
DATABASE_URL |
✅ | — | MySQL 连接字符串(Prisma) |
REDIS_URL |
✅ | — | Redis 连接字符串(ioredis) |
ADMIN_SECRET |
✅ | — | 管理后台密钥(生产必改) |
JWT_SECRET |
— | dev-secret-... |
JWT 签名密钥 |
BID_RATE_LIMIT_PER_SECOND |
— | 3 |
单用户每秒最大出价次数 |
BID_IDEMPOTENCY_TTL_MS |
— | 3000 |
幂等窗口(毫秒) |
AUCTION_EXTEND_THRESHOLD_SECONDS |
— | 10 |
延时触发阈值(秒) |
⚠️ 注:为规避部分主机默认端口占用问题,本项目默认将 MySQL 映射至 13306,Redis 映射至 16379。
详细接口规范见 docs/api_protocol.md
| 方法 | 路径 | 说明 |
|---|---|---|
GET |
/api/auctions |
大厅竞拍列表(active/pending,MySQL+Redis 合并) |
GET |
/api/auctions/:id/info |
竞拍静态信息(商品名、图片、起拍价) |
GET |
/api/auctions/:id/state |
实时状态(Redis HGETALL,< 1ms) |
POST |
/api/auctions/:id/bid |
提交出价(核心高频接口,走 Lua 原子脚本) |
POST |
/api/auctions/test-init |
测试初始化(仅开发环境) |
| 方法 | 路径 | 说明 |
|---|---|---|
GET |
/api/admin/auctions/latest |
最新竞拍实时概览 |
GET |
/api/admin/auctions/:id |
按 ID 查竞拍概览 |
POST |
/api/admin/auctions |
发布真实竞拍 |
POST |
/api/admin/auctions/init |
一键初始化测试竞拍(兼容旧接口) |
POST |
/api/admin/auctions/:id/cancel |
取消竞拍(广播 auction_cancelled) |
POST |
/api/admin/auctions/:id/end |
提前结束竞拍(广播 auction_ended) |
| 方向 | 事件名 | Payload |
|---|---|---|
| C→S | join_auction |
{ auctionId: string } |
| C→S | leave_auction |
{ auctionId: string } |
| C→S | request_state |
{ auctionId: string } |
| S→C | joined_success |
{ auctionId, serverTime } |
| S→C | bid_update |
{ auctionId, currentPrice, winnerId, endTimeMs, bidCount, isExtended, leaderboard } |
| S→C | auction_state |
{ auctionId, currentPrice, ..., serverTime, productName, leaderboard } |
| S→C | auction_ended |
{ auctionId, winnerId, finalPrice } |
| S→C | auction_cancelled |
{ auctionId, reason } |
问题:多用户同时出价时,传统「查询→判断→更新」模式存在竞态条件(Race Condition),可能导致超卖或数据不一致。
解决:将限流、幂等、状态校验、价格比较、状态更新等 8 个步骤封装为单个 Redis Lua 脚本原子执行。所有金额以**分(整数)**存储和比较,规避浮点精度问题。时间权威采用服务端传入的时间戳,不依赖 Redis 内部时钟。
问题:客户端时钟不可信任(用户可能修改系统时间),且网络延迟导致服务端与客户端存在不确定的时差。
解决:设计三层时钟校准协议。joined_success 事件下发服务端时间戳,前端计算 serverTimeDelta 记录偏差。所有需精确时间的组件使用 Date.now() - serverTimeDelta 替代原生时间,将倒计时误差收敛至网络 RTT 级别。
问题:用户网络波动导致 WebSocket 断开后重连,期间可能错过多条 bid_update 广播(价格变化、延时触发等),直接显示旧数据会导致 UI 与实际状态不一致。
解决:采用双路径夹投机制。路径一:收到 request_state 事件后,服务端从 Redis 全量读取状态并仅向请求者发送 auction_state(安静重连)。路径二:前端本地 setInterval 轮询检测超时。配合 winnerModalFiredRef 互斥锁,可靠应对广播延迟或丢包场景。
问题:发布竞拍需要同时写入 MySQL(商品+场次)和 Redis(实时状态)。若 MySQL 写入成功但 Redis HSET 失败(如网络瞬断),数据库中留下"幽灵竞拍"——存在记录但实际不可用。
解决:在 Service 层引入补偿事务回滚机制。采用 MySQL → Redis 的写入顺序,若 Redis hset 抛出异常,主动捕获并执行 Prisma delete 删除已创建的 MySQL 记录,保障最终一致性。
- DeepSeek V4 — 架构设计讨论、技术选型评审
- Claude Sonnet 4.6 — 核心代码生成(后端 API、Lua 脚本、React 组件)
- Gemini 3.5 Pro Preview — 代码审查、文档生成
本项目采用「主文档驱动 + 分步精准投喂」的 Prompt 工程策略:
-
阶段 0-1(基建 + 数据库):先由 AI 生成 MySQL DDL 和 Redis 数据结构规范文档,作为后续所有阶段的 Context 锚点,确保类型系统一致
-
阶段 2(核心 API):将 DDL 和 Redis 规范作为上下文,要求 AI 生成 Node.js 接口和 Redis Lua 脚本。关键约束:金额统一以分(整数)存储,全程整数比较
-
阶段 3(WebSocket):在阶段 2 代码基础上增量注入,锁定 Socket 事件契约(
SOCKET_EVENTS常量),前后端共用类型定义消除接口漂移 -
阶段 4(前端):以 API 规范文档(
api_protocol.md)为红线,约束前端 AI 不得假设后端未实现的字段,实现视觉组件与数据层的严格分离
- 每阶段生成前,强制 AI 读取前序阶段的输出文件作为 Context
- 类型定义统一在
src/types/index.ts中维护,前后端共用 - 关键业务规则(金额单位、时间戳格式)在 Prompt 中以「红线」形式约束
以下数据来自本地压测环境(Windows 10,Docker 容器),测试竞拍 ID=29
| 指标 | 数值 | 测试条件 |
|---|---|---|
| 出价接口 P50 延迟 | 8.6 ms | 单用户串行 10 次出价 |
| 出价接口 P99 延迟 | 12.4 ms | 单用户串行 |
| 出价接口 P50 延迟 | 61 ms | 50 并发(含限流中间件处理) |
| 出价接口 P99 延迟 | 108 ms | 50 并发 |
| 出价接口峰值 QPS | 768 req/s | autocannon 50 并发 10s(含限流拦截) |
| WebSocket 20 连接建立 | 67 ms | 全部连接完成 |
| WebSocket 20 加入房间 | 74 ms | 全部 joined_success 完成 |
| Redis PING 延迟 | < 1 ms | Docker 内部回环 |
| Redis 内存占用 | 1.25 MB / 256 MB | 空闲状态 |
⚠️ 并发压测中,单用户 3 次/秒限流拦截了大部分请求,实际 QPS 反映的是限流中间件的处理能力而非完整出价流程。完整压测报告见 docs/benchmark.md。
MIT License — 详见 LICENSE 文件。
Made with ❤️ by AI + Human collaboration