秒杀系统高并发优化实战(C++ / Drogon):4.4 压测:100 用户抢 10 件,核对不超卖
4.3 把"原子扣减 + 事务 + 幂等"写完了,但代码说"不超卖"不算数——压测说不算超卖才算数。这一篇用压测 harness 跑"100 用户抢 10 件",再用 MySQL 直接核对 sold == orders == 10,证明原子 UPDATE ... WHERE stock>0 在高并发下真的扛住了。
配套脚本:
scripts/jmeter-baseline.sh(correct 模式)、scripts/smoke-seckill.sh。核对 SQL 见 4.3 / README「快速查看数据」。
一、压测目标:用"卖出去的件数"反证不超卖
超卖的定义就一句话:卖出的件数 > 库存。所以验证不超卖,不是看返回码,而是看 MySQL 里的真相源:
1 | -- 卖出件数 = 总量 - 剩余;订单数 = 落库行数。两者必须相等,且 ≤ 总量 |
二、压测 harness:correct 模式
scripts/jmeter-baseline.sh 支持一个 correct 模式:100 个并发用户抢 10 件库存,专为"核对不超卖"设计。它优先用官方 Apache JMeter(JMETER_BIN 环境变量启用,.jmx 采样器 implementation=Java 绕过 WSL 上 HTTPHC4Impl 初始化失败),无 JMETER_BIN 时回退零依赖 curl 并发 harness(timeout + xargs -P + curl -w,awk 算 QPS/百分位)。
1 | # 100 用户抢 10 件(库存先 reset 成 10),correct 模式 |
correct 模式的判定逻辑:每个请求拿返回码,统计 200 / 409(SOLD_OUT) / 409(DUPLICATE_ORDER) 分布;最终回 MySQL 核对 sold == orders。只要 sold > 10 或 sold != orders,立即报"超卖"。
三、结果:10×200 + 90×409,sold=orders=10
100 并发抢 10 件,结果清晰:
| 返回 | 数量 | 含义 |
|---|---|---|
| 200 | 10 | 真正买到 |
409 SOLD_OUT |
90 | 库存归零后被拦下 |
409 DUPLICATE_ORDER |
0 | 本场景每人只发一次,无重复 |
| sold(卖出件数) | 10 | total - stock = 10 |
| orders(订单数) | 10 | COUNT(*) = 10 |
sold == orders == 10,且 10 ≤ 库存 10——零超卖。这证明 4.3 的"原子 UPDATE ... WHERE stock>0"在 100 并发下确实把"是否还有库存"锁在了行锁内判定,没有一个人读到旧值多扣。
压测工程坑(WSL 实测,值一圈):
- IPv4 坑:Java 把
localhost解析成 IPv6::1,而 Drogon 只监听0.0.0.0:8080→ JMeter 连localhost100%connection refused。.jmx的HTTPsampler.domain必须写127.0.0.1;curl 也统一127.0.0.1。- Java 代理坑:本机若设了
http_proxy(如127.0.0.1:9305),Java 会把它当http.proxyHost,把 JMeter 每个请求路由到代理端口 → 同样 100%connection refused、0ms、吞吐虚高。脚本跑官方 JMeter 前unset http_proxy https_proxy且export JVM_ARGS="... -Dhttp.proxyHost= -Dhttps.proxyHost= ..."。curl 不受影响(走NO_PROXY=127.0.0.1)。
四、为什么这个测试能证明不超卖
100 个请求"同时"到达,若扣减是"先 SELECT 再 UPDATE",会有大量请求在 stock 还显示 10 时就通过判断,最终卖出远多于 10 件。而原子 UPDATE ... WHERE stock>0 让数据库在行锁内逐个判定:第 11 个及以后的请求 affectedRows==0 → 直接 SOLD_OUT 回滚。行锁把并发"串行化"在单行上,这才是 sold 永远 ≤ 10 的原因——也是阶段一吞吐被卡的根(见 4.7)。
功能抉择(本篇核心权衡)
① 为什么验证不超卖要看 MySQL 而不是看返回码?
返回码是应用层的"自报"——万一代码有 bug(比如扣减没真进事务),返回 200 但库存没动,看返回码会误判"成功"。sold == orders 来自数据库真相源,是超卖的唯一权威判据。
② 为什么用 correct 模式(固定 100 抢 10)而不是纯灌 QPS?
纯灌 QPS 测的是吞吐,看不出正确性。correct 模式把"库存设小、并发设大",逼出"边界竞争",专门验证原子性——吞吐和正确性是两个维度,阶段一先保证正确性(不超卖),再在 4.7 量吞吐。
③ 为什么同时备 curl harness 和官方 JMeter?apt install jmeter 在 WSL 是老/缝合怪包(不支持 -e -o、加载 .jmx 报 xstream ForbiddenClassException),不可用;官方 JMeter 二进制支持 -e -o 直接出 HTML 报告。无 JMETER_BIN 时 curl harness 零依赖交叉验证,两条通道结果一致才可信。
小结
- 验证不超卖的唯一权威判据是 MySQL 真相源:
sold == orders且≤ 库存。 - correct 模式 100 抢 10 → 10×200 + 90×409(SOLD_OUT),
sold=orders=10,零超卖。 - 原子
UPDATE ... WHERE stock>0靠行锁串行化把并发判定锁在单行,这是sold永 ≤ 10 的原因。 - 压测工程坑记牢:
localhost→127.0.0.1(IPv4)、跑 JMeter 前unset代理;双通道(curl + 官方 JMeter)交叉验证。🐾

