一旦系统被压到临界点你最先遇到的往往不是数据库慢查询、也不是Redis雪崩而是那个最不起眼却最致命的问题同一个请求被客户端重试了多次结果产生了多笔订单、多扣了钱、多条脏数据。在高并发场景下接口幂等性不是一个“要不要做”的问题而是“不做就会出事”的底线问题。不管你是做支付、下单、转账还是简单的表单提交只要接口暴露在网络环境中就必须假设客户端会重复调用可能是用户手抖点了几下可能是网关超时自动重试可能是消息队列的at-least-once语义导致消费端重复执行。这中间任何一个环节都可能让你的接口被执行两次以上。这篇内容不打算泛泛而谈概念而是把我自己在实际项目中用过的、踩过的、验证过的幂等方案完整讲一遍。从问题本质出发逐个拆解主流方案的原理和坑最后给出落地的组合策略。适合正在处理高并发业务的开发者、后端工程师以及被“重复提交”问题折磨过的所有人。1. 幂等问题的本质为什么高并发下最容易暴露很多人有个误区觉得幂等问题是“做不做”的选择题其实不然。接口本身是否幂等取决于它对系统状态的改变。一个纯查询接口天然幂等读一万次和读一次结果一样。但凡是涉及写操作的接口比如创建订单、扣减库存、支付回调、锁定优惠券重复执行就会产生重复的状态变更。在高并发环境下这个问题的暴露概率会被急剧放大。原因有三点。第一网络超时后的自动重试。客户端发起请求服务端处理成功但响应超时客户端认为失败了于是重发。这时候服务端收到的其实是同一个业务请求的两次执行。如果接口不幂等这笔业务就被处理了两遍。第二网关或框架层的重试机制。很多RPC框架自带重试策略比如Dubbo默认会在超时后重试Spring Cloud的Ribbon也有重试配置。业务代码还没意识到发生了重复框架已经把同一请求又发过来了。第三消息队列的重复消费。RabbitMQ、Kafka这类中间件为了保证消息不丢普遍采用at-least-once投递语义也就是“至少一次”消费。这意味着消费端在处理消息时必须自己实现“只处理一次”的语义否则积压重试、重新平衡都会带来重复执行。这三个场景叠加在一起会让你的后端接口在高峰期收到大量“长得一模一样”的请求。真正需要你回答的问题是当并发量10000、同一笔订单同时来10次相同请求时你的代码能不能保证只有1次生效1.1 什么叫“同一个请求”两个维度要分清谈幂等方案之前先要把“同一个请求”的定义搞清楚。不是所有参数一样的请求都算同一个我们需要从两个维度判断。第一个维度是业务维度。比如用户下单时前端生成的订单号是唯一标识回复说“订单号重复了”。按钮事件绑定的是同一个id第一次点击后还没有即时disable。另外还有用户双击、GoBack后重新点击等前端防不住。缓存的更新时机预扣库存时发起支付支付成功后如果回调处理失败库存未扣减成功。要把库存的占用状态和订单的支付状态区分存储用独立的缓存键如stock:preserve:{skuId}和stock:paid:{skuId}并允许它们在极端情况下短暂不一致最终通过异步对账来修正。【问题12】并发下同一用户同时提交了多个订单但token被重复使用了怎么处理令牌在Redis中取出和删除不是一个原子操作两个并发的请求同时读到同一个token都验证成功都执行业务。解决方案是把验证和删除封装为原子操作通过Lua脚本实现“get后删除若不存在则失败”或者直接用Redis事务。这也是为什么我上面强调要用EVALSHA。5. 最后再分享一个实际经验这套组合方案我在一个日订单量百万级的系统里实际运行过。最初只用了数据库唯一键后来发现订单号冲突的报警太频繁加了token机制后前端重复点击的问题解决了但MQ消费端的重复消费问题仍然存在最终上状态机方案才把“已支付订单再次退款”的极端问题彻底堵住。真正让我觉得踏实的是明白了“幂等不是一个功能点而是一种系统设计视角”。它渗透在接口设计的每个细节请求ID怎么生成、数据库索引怎么建、缓存何时写入、状态如何流转、异常怎么处理。另外还有一点特别想说的是幂等方案不能一次做死。业务形态在变订单会新增状态、会引入新的消息类型、会接入新的调用方。比较合理的做法是在核心链路上预留统一的幂等处理组件把“请求指纹生成”“幂等校验”“结果返回”抽象为可复用环节。这样新增业务时只需要注册新的幂等场景而不需要重新写一套逻辑。最后再补充一个小技巧上线幂等机制前一定要写好灰度方案。我一般是针对新接口先开启幂等校验观察日志中重复请求占比和业务异常率的曲线确认稳定后再逐步全量。不要一上来就硬切否则一旦幂等设计有误线上会陷入大量请求被“误判为重复”的危机里。接口幂等就讲到这里。方案也好代码也好都不复杂复杂的是你怎么看待这个问题、怎么为极端情况留好退路。希望这篇实战拆解能帮你少踩几个坑。