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
2
3
4
-- 卖出件数 = 总量 - 剩余;订单数 = 落库行数。两者必须相等,且 ≤ 总量
SELECT (SELECT total FROM seckill_sku WHERE id=1) - (SELECT stock FROM seckill_sku WHERE id=1) AS sold,
(SELECT COUNT(*) FROM seckill_order WHERE sku_id=1) AS orders;
-- 不超卖 ⇔ sold = orders 且 sold ≤ 10

二、压测 harness:correct 模式

scripts/jmeter-baseline.sh 支持一个 correct 模式:100 个并发用户抢 10 件库存,专为"核对不超卖"设计。它优先用官方 Apache JMeterJMETER_BIN 环境变量启用,.jmx 采样器 implementation=Java 绕过 WSL 上 HTTPHC4Impl 初始化失败),无 JMETER_BIN 时回退零依赖 curl 并发 harnesstimeout + xargs -P + curl -w,awk 算 QPS/百分位)。

1
2
3
# 100 用户抢 10 件(库存先 reset 成 10),correct 模式
bash scripts/jmeter-baseline.sh correct 100 10 1
# → 期望:10 个 200(买到)、90 个 409(SOLD_OUT)、sold=orders=10

correct 模式的判定逻辑:每个请求拿返回码,统计 200 / 409(SOLD_OUT) / 409(DUPLICATE_ORDER) 分布;最终回 MySQL 核对 sold == orders。只要 sold > 10sold != 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 实测,值一圈):

  1. IPv4 坑:Java 把 localhost 解析成 IPv6 ::1,而 Drogon 只监听 0.0.0.0:8080 → JMeter 连 localhost 100% connection refused.jmxHTTPsampler.domain 必须写 127.0.0.1;curl 也统一 127.0.0.1
  2. Java 代理坑:本机若设了 http_proxy(如 127.0.0.1:9305),Java 会把它当 http.proxyHost,把 JMeter 每个请求路由到代理端口 → 同样 100% connection refused、0ms、吞吐虚高。脚本跑官方 JMeter 前 unset http_proxy https_proxyexport 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 的原因。
  • 压测工程坑记牢:localhost127.0.0.1(IPv4)跑 JMeter 前 unset 代理;双通道(curl + 官方 JMeter)交叉验证。🐾