秒杀系统高并发优化实战(C++ / Drogon):4.5 压测:验证重复下单(uk_user_sku 幂等,0 重复)
超卖防住了(4.4),第二个要命的问题是重复下单——同一用户连点 N 次,刷出 N 单。这一篇专门压"重复":同一个 (userId, skuId) 并发发一堆请求,再用 MySQL 核对"这个用户只有 1 张订单",证明 uk_user_sku + ON DUPLICATE KEY 的幂等兜住了。
配套代码:
sql/schema.sql(uk_user_sku)、src/service/SeckillService.cc(步骤 2 的ON DUPLICATE KEY UPDATE)、scripts/jmeter-baseline.sh的并发重复场景。
一、重复下单的两层防护
重复下单有两层防线,各管一段:
- 应用层在途闸门(4.8 的
InflightGuard):同一(userId, skuId)已有请求在处理时,后续请求连 DB 都不打直接判DUPLICATE_ORDER。挡的是"并发窗口内的重复"。 - 数据库唯一键
uk_user_sku:串行发出的两次下单(第一次已完成、订单已落库),应用层闸门拦不住,由唯一键兜底——第二次 INSERT 撞uk_user_sku→ON DUPLICATE KEY命中 →affectedRows==0→ 判重复 + 回滚还库存。挡的是"一切重复",是正确性底线。
分工要记牢:应用层闸门只挡并发窗口内的重复;串行发出的两次下单由
uk_user_sku唯一键兜底。两层各管一段,缺一不可。
二、压测场景:同一用户对同一商品并发 N 次
构造"恶意连点":固定一个 userId,对它并发发 200 个下单请求(库存充足,先排除售罄干扰),看最终落库几张订单。
1 | # 同一 userId=151 并发 200 次抢 skuId=1(库存足够),验证只成 1 单 |
核对 SQL(权威判据):
1 | SELECT COUNT(*) AS orders FROM seckill_order WHERE user_id = 151 AND sku_id = 1; |
三、结果:0 重复,orders 恒等于 1
| 返回 | 数量 | 含义 |
|---|---|---|
| 200 | 1 | 第一笔成功 |
409 DUPLICATE_ORDER |
N−1 | 并发窗口内被应用层闸门 / 唯一键拦下 |
409 SOLD_OUT |
(若库存不够时) | 库存耗尽 |
| orders(该用户订单数) | 1 | COUNT(*) = 1 |
orders == 1,零重复。即便 200 个请求"同时"到达,数据库唯一键保证只有一行能插入成功,其余全部 affectedRows==0 被判重复并回滚还库存——绝不会多卖一件给同一用户。
四、为什么唯一键是"底线"而非"唯一手段"
应用层闸门(4.8)能挡掉大部分并发重复、省 DB 往返,但它不是权威:进程重启、token 拷贝、或精确错开并发窗口的两次请求,应用层都会放过。真正兜底的永远是 uk_user_sku——它不依赖任何应用层状态,是数据库层面的硬约束。所以即便 4.8 的闸门关掉(mode=none,即阶段一基线),重复下单仍然是 0,只是更多请求会打到 DB 去撞唯一键(见 4.8 实测:none 模式 dup 场景挡掉=0,但 orders 仍=1)。
功能抉择(本篇核心权衡)
① 为什么幂等用数据库唯一键而不是"应用层先查再插"?
应用层"先 SELECT 看有没有订单再 INSERT"在并发下会竞态:两个请求同时查到"没有",都去插,照样重复。uk_user_sku 把"是否已有订单"交给数据库单行约束,撞键即 affectedRows==0,从根消除竞态——这是正确性底线,不依赖任何应用层状态。
② 为什么还要 4.8 的应用层闸门,既然唯一键已经兜底?
唯一键兜底意味着"重复请求也会打到 DB 去撞键、占连接、持行锁"——200 个重复请求里有 199 个在白白消耗 DB 资源。应用层闸门在并发窗口内就把它们挡在 DB 之外(见 4.8 实测:dup 场景挡掉≈1500,DB 写压力降 ~75%)。两层分工:应用层省压力,唯一键保正确。
③ 为什么撞唯一键后必须回滚而非忽略?
步骤 1 已经把库存扣了。若撞键后不回滚,库存会被这次"假重复"白白吃掉一件,且没有任何订单对应——直接造成"少卖 + 数据不一致"。ON DUPLICATE 命中后显式 tx->rollback(),把多扣的库存还回去,账才平。
小结
- 重复下单有两层防护:应用层在途闸门(挡并发窗口内的,省 DB)+
uk_user_sku唯一键(兜底一切,正确性底线)。 - 压测:同一用户并发 200 次 → orders 恒等于 1,零重复。
- 唯一键是权威兜底:不依赖应用层状态,进程重启/错开窗口都拦得住;撞键
affectedRows==0→ 回滚还库存。 - 应用层闸门(4.8)在并发窗口内挡掉重复请求省 DB 压力,但不替代唯一键。🐾

