超卖防住了(4.4),第二个要命的问题是重复下单——同一用户连点 N 次,刷出 N 单。这一篇专门压"重复":同一个 (userId, skuId) 并发发一堆请求,再用 MySQL 核对"这个用户只有 1 张订单",证明 uk_user_sku + ON DUPLICATE KEY 的幂等兜住了。

配套代码:sql/schema.sqluk_user_sku)、src/service/SeckillService.cc(步骤 2 的 ON DUPLICATE KEY UPDATE)、scripts/jmeter-baseline.sh 的并发重复场景。

一、重复下单的两层防护

重复下单有两层防线,各管一段:

  1. 应用层在途闸门(4.8 的 InflightGuard:同一 (userId, skuId) 已有请求在处理时,后续请求连 DB 都不打直接判 DUPLICATE_ORDER。挡的是"并发窗口内的重复"。
  2. 数据库唯一键 uk_user_sku:串行发出的两次下单(第一次已完成、订单已落库),应用层闸门拦不住,由唯一键兜底——第二次 INSERT 撞 uk_user_skuON DUPLICATE KEY 命中 → affectedRows==0 → 判重复 + 回滚还库存。挡的是"一切重复",是正确性底线

分工要记牢:应用层闸门只挡并发窗口内的重复;串行发出的两次下单由 uk_user_sku 唯一键兜底。两层各管一段,缺一不可。

二、压测场景:同一用户对同一商品并发 N 次

构造"恶意连点":固定一个 userId,对它并发发 200 个下单请求(库存充足,先排除售罄干扰),看最终落库几张订单。

1
2
3
# 同一 userId=151 并发 200 次抢 skuId=1(库存足够),验证只成 1 单
bash scripts/jmeter-baseline.sh dup 151 200 1
# → 期望:1 个 200,其余 199 个 409(SOLD_OUT 或 DUPLICATE_ORDER),orders=1

核对 SQL(权威判据):

1
2
SELECT COUNT(*) AS orders FROM seckill_order WHERE user_id = 151 AND sku_id = 1;
-- 幂等通过 ⇔ orders = 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)。

图 1:重复下单的两层防护(应用层闸门 + 数据库唯一键) ① 应用层在途闸门(4.8)并发窗口内的重复连 DB 都不打,直接 DUPLICATE省压力,非权威 ② uk_user_sku 唯一键(底线)一切重复(含错开窗口)ON DUPLICATE → affectedRows=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 压力,但不替代唯一键。🐾