一秒点了八次
重复请求的来源比想象的多:用户连点是显性的——抢不到就狂点,一秒八次;网络重试是隐性的——网关或客户端超时自动重发,同一个请求在路上走了两遍;脚本多打是恶意的——同一账号并发发十几条请求,哪条成功算哪条。三个来源叠加,不做幂等的秒杀系统,一个用户抢到三台手机只是时间问题。
幂等令牌:一次性消费
进入秒杀页时给用户签发一枚幂等令牌(token),下单必须携带,消费一次即作废——用 Redis 的原子写实现:SETNX 成功才放行。同一 token 的第二次请求直接判重复拒绝。token 绑定用户与活动,防转让防批量;有效期覆盖一次完整下单流程即可,过期作废重新签发。这一层挡掉的是绝大多数重复:连点与重试都带同一个 token,天然被拦截。
唯一索引:物理层的最后防线
令牌是逻辑约束,逻辑总有漏网的时刻——token 校验与订单落库之间,并发的两个请求可能都通过了校验。数据库唯一索引是物理约束:订单表对(用户 ID + 活动 ID)建唯一索引,"一人一单"变成数据库的物理保证,任何漏网请求在落库那一刻被唯一键冲突拦下,捕获冲突返回"已抢到"即可。
// 第一层:幂等令牌,一次性消费
Boolean ok = redis.setIfAbsent("tk:" + token, "1", Duration.ofMinutes(10));
if (Boolean.FALSE.equals(ok)) {
return fail(Result.DUPLICATE); // token 已消费:重复请求
}
// 第二层:订单表唯一索引,一人一单的物理保证
ALTER TABLE seckill_order
ADD UNIQUE KEY uk_user_activity (user_id, activity_id);
// 落库捕获冲突:不是报错,是"已经抢到了"
try { orderMapper.insert(order); }
catch (DuplicateKeyException e) { return success(existing(order)); }两层防线各管一段
两层幂等的职责要分清:令牌层在入口挡量——重复请求没到扣库存那一步就被拦下,保护的是 Redis 与 MQ 的压力;索引层在终点兜底——正确性不依赖任何上层逻辑,哪怕令牌层全部失效,重复订单也插不进去。正确性交给最底层,性能优化交给上层——这个分层原则适用于所有防御性设计。顺带一提,回补库存、核销优惠这些"加法"动作同样要过幂等闸门,复用同一套 token 或记录表机制。
并发的细缝
有人会问:同一用户的两个请求同时到达,会不会都过了 SETNX?不会——SETNX 是原子操作,两个并发只有一个成功。会不会都过了索引?也不会——唯一索引在数据库层用锁保证冲突可见。真正要防的是校验与执行跨了两个存储又没有共同闸门的设计:比如令牌在 Redis、订单在 MongoDB,两边各管各的——只要幂等的两层都落在"有原子能力"的存储上,细缝就不存在。数据防住了,还要证明它真的防住了——下一篇讲对账:Redis 与 DB 的最终一致。
评论 (0)