4.1~4.6 把基础秒杀的下单链路(列表 / 详情 / 下单 / 不超卖 / 不重复 / 超卖路线抉择)全部跑通并验证了正确性。这一篇给阶段一打一个可量化的基线:到底能扛多少 QPS、延迟长什么样、超卖/重复到底有没有。基线是后面每一次优化的"参照物"——没有它,你根本说不清"阶段二缓存预扣减到底快了多少"。

配套资产:scripts/jmeter-baseline.sh(双通道压测)、jmeter/seckill-baseline.jmx(官方 JMeter 脚本)、jmeter/out/report/index.html(HTML 报告)。压测在 WSL(i7-14650HX + 本地 MySQL)由真机跑出。

一、为什么必须先打基线

整个项目的验收硬指标是:每一阶段 QPS 提升一个量级 + 不超卖 + 不重复下单。阶段一(v0.1.x)是"Drogon + MySQL 直连"的最朴素形态,它的任务不是冲吞吐,而是:

  1. 把正确性钉死:不超卖、不重复下单,端到端可复现地证明;
  2. 建立延迟与吞吐基线:给后续阶段一个对比锚点(每步优化都应显著拉高 QPS、压低延迟)。

所以基线的验收标准反过来也简单:QPS 是多少不重要,0 超卖 + 0 重复必须先达成,再记录数字。

二、压测怎么搭:双通道 + 两种模式

scripts/jmeter-baseline.sh 设计成双引擎两种模式,是为了在不同环境都能出数字、且互相校验。

两种模式

  • baseline:库存置 1e8(60s 内绝不会售罄),专测"卖出"事务路径的稳态 QPS 与延迟。
  • correct:库存置 10、100 并发抢,核对不超卖(要求 sold == orders == 10)。

双通道(引擎优先级)

  • 装有官方 Apache JMeter(非 apt 缝合怪包)时,走 JMETER_BIN 指向的二进制,出 HTML 报告;
  • 否则回退零依赖 curl 并发 harnesstimeout + xargs -P + curl -w,awk 算 QPS/百分位),保证任何机器都能出基线。

WSL 实测踩过的三个坑(已在脚本里修掉):① Java 把 localhost 解析成 IPv6 ::1,而 Drogon 只监听 IPv4 0.0.0.0:8080 → 脚本统一改 127.0.0.1;② 本机 http_proxy 被 Java 当 http.proxyHost 路由到代理端口 → 跑 JMeter 前 unset 代理 + JVM_ARGS 关代理;③ 官方 JMeter 5.6.x 默认 HTTP 实现 HTTPHC4Impl 在 WSL 下静态初始化失败 → .jmx 采样器显式 implementation=Java 走 JDK HttpURLConnection,功能等价、正常出样本。

三、基线结果

官方 JMeter(走 implementation=Java,100 线程 / ramp 10s / 持续 60s 打 /api/seckill

指标 官方 JMeter 实测 说明
样本数 22352 实际发出的请求
吞吐 QPS 371.03/s(≈371) 阶段一基线
错误率 0% 0 超卖、0 失败
mean / median 246.8 / 251 ms 平均与中位延迟
p90 / p95 / p99 333 / 359 / 399 ms 尾延迟
max 436 ms 最慢一次

curl harness 交叉验证(口径一致、独立实现):QPS≈341.3,avg 0.2888s / p50 0.2736s / p95 0.3752s / p99 0.4847s;200=20479、409=0、失败=0;MySQL sold == orders == 20492(0 超卖)。两套 harness 差约 9%,属并发模型差异,量级一致,相互背书。

图 1:官方 JMeter 延迟百分位(ms) 0 ms → 251median 333p90 359p95 399p99 436max 尾延迟集中在 330~400ms,瓶颈在 DB 行锁等待(见第四节)

correct 模式铁证(不超卖):100 并发抢 10 件库存 → 返回 10×200 + 90×409,MySQL 核对 sold=10 / orders=10。原子 UPDATE ... WHERE stock>0 + uk_user_sku 幂等在端到端层面实锤生效。

四、根因:为什么卡在 ~371 QPS

基线 ~371 不是偶然,根因很明确:每个请求都占一条 DB 连接,并持有 seckill_sku 这一行的行锁直到事务提交

图 2:请求串行化在单行锁上 请求 A 请求 B 请求 C seckill_sku 单行 行锁(持到事务提交) A 持锁时 B/C 排队 ↑ 串行化 = 吞吐瓶颈

所有请求在 seckill_sku 这一行上排队过行锁,吞吐被锁的持有时间(含 MySQL 往返)严格限制——这正是"DB 直扣"方案的共性代价,也是阶段二(Redis 预扣减)、阶段三(MQ 异步)、阶段四(Lua 原子)要逐一拆掉的靶心。基线的意义就在这里:它把"哪里慢"标得清清楚楚。

五、阶段一总结

阶段一代码侧 + 压测侧已全部闭环。落地清单:

模块 内容 关键设计
基础秒杀 列表 / 详情 / 下单 UPDATE ... WHERE stock>0 防超卖;事务内扣库存+落订单;ON DUPLICATE KEY UPDATE + uk_user_sku 幂等
登录鉴权 注册 / 登录 / 登出 PBKDF2 加盐哈希;自实现 HS256 JWT;Redis 可吊销会话(sess:{jti} SETEX + 吊销 DEL)
短信验证码 发送 / 校验 / 加固 腾讯云 API 3.0 直连(TC3-HMAC-SHA256 自签);Lua 原子校验 + 每日上限 + 试错上限
登录加固 失败计数 / 临时锁定 Redis 共享失败计数 + Lua 原子阈值 + 临时锁定
应用层锁 在途闸门 InflightGuard mutex / 自旋 / 原子三后端,DB 之前挡掉并发重复(4.8 详测)

架构骨架(Drogon + MySQL 直连):

  • 控制器只做协议转换(HTTP ↔ 业务参数),业务逻辑下沉到 service/
  • 统一 JSON 响应经 src/util/HttpJson.h 收敛;
  • Redis 缺失时登录/短信返 503,秒杀主链路不受影响(fail-open);
  • /api/seckill 暂不接 token 校验(阶段一为压测便利,鉴权接入留待前端阶段)。

验收对照:不超卖(sold==orders,correct 模式 10==10)✅;不重复下单(uk_user_sku 兜底,orders 恒=1)✅;基线 QPS≈371 / 0 超卖 ✅。三项目标全达成。

小结

  • 阶段一基线:官方 JMeter QPS≈371 / p95≈359ms / 0 超卖;curl harness 交叉验证 ≈341,量级一致、互相背书。
  • 不超卖在 correct 模式实锤:100 抢 10 → 10×200 + 90×409sold==orders==10
  • 根因:每个请求持有 seckill_sku 单行锁到事务提交 → 行锁串行化 = 吞吐瓶颈,正是后续阶段的优化靶心。
  • 阶段一验收三目标(不超卖 / 不重复 / 基线可量化)全部达成,代码侧 + 压测侧闭环。🐾