连载中 11/15

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

2026-09-07 · 413 阅读 · 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)

热门推荐

连载中 11/22

主从搭建实操:从零配出一主两从

光讲原理不过瘾?手把手搭一主两从:my.cnf 六个参数、复制账号、GTID、CHANGE REPLICATION SOURCE TO、SHOW REPLICA STATUS 验收,附翻车排查清单。

#MySQL#主从复制#GTID#主从搭建#高可用
2026-05-07 · 10101 阅读 · 0 评论 · 0 赞
连载中 16/22

连接池:HikariCP 参数与连接风暴

连接池不是越大越好:8 核机器配 1000 连接反而更慢的数学原理,HikariCP 四个必调参数,maxLifetime 与 wait_timeout 的隐形陷阱。

#MySQL#连接池#HikariCP#maxLifetime#连接风暴
2026-05-10 · 9873 阅读 · 0 评论 · 0 赞
连载中 4/16

缓存穿透:恶意 ID 打穿 MySQL 的四道防线

请求的数据在缓存和数据库里都不存在时,缓存形同虚设。聊聊参数校验、空值缓存、布隆过滤器、限流熔断四道防线的原理与组合打法。

#Redis#缓存穿透#布隆过滤器#高可用
2026-05-16 · 9294 阅读 · 21 评论 · 287 赞