连载中 11/15

幂等与一人一单:重复请求的全量防御

2026-09-07 · 414 阅读 · 0 评论 · 0 赞

一秒点了八次

重复请求的来源比想象的多:用户连点是显性的——抢不到就狂点,一秒八次;网络重试是隐性的——网关或客户端超时自动重发,同一个请求在路上走了两遍;脚本多打是恶意的——同一账号并发发十几条请求,哪条成功算哪条。三个来源叠加,不做幂等的秒杀系统,一个用户抢到三台手机只是时间问题

幂等令牌:一次性消费

进入秒杀页时给用户签发一枚幂等令牌(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 的最终一致

503

10 年全栈工程师 · 503咖啡馆主理人

#幂等设计#一人一单#幂等令牌#唯一索引#重复请求

评论 (0)

相关推荐

连载中 12/20

排查四件套:jstack、jmap、jstat、jcmd 的实战分工

jstack 看线程在干什么,jmap 看堆里装了什么,jstat 看运行时在变什么,jcmd 是统一入口。四把刀各管一段,配合着用没有查不动的现场。

#jstack#jmap#jstat#jcmd#排查工具
2026-09-16 · 5 阅读 · 0 评论 · 0 赞
连载中 11/20

GC 日志:把回收过程翻译成人话

一行 GC 日志里塞着七种信息:谁触发的、收了哪、停了多久、活了哪些。加上 -Xlog 配置,再加上日志分析工具,GC 不再是只能盯监控曲线的黑盒。

#GC日志#Xlog#日志分析#GC监控#Full GC排查
2026-09-16 · 13 阅读 · 0 评论 · 0 赞
连载中 10/20

ZGC:亚毫秒停顿是怎么炼成的

百 G 大堆停顿不到一毫秒,靠的是把搬家全部挪到并发阶段——着色指针让引用自带状态,读屏障让搬运中的对象依然可访问。代价是吞吐与内存,收益是停顿与堆大小解耦。

#ZGC#着色指针#读屏障#亚毫秒停顿#分代ZGC
2026-09-15 · 8 阅读 · 0 评论 · 0 赞