一旦系统被压到临界点,你最先遇到的往往不是数据库慢查询、也不是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怎么生成、数据库索引怎么建、缓存何时写入、状态如何流转、异常怎么处理。
另外还有一点特别想说的是,幂等方案不能一次做死。业务形态在变,订单会新增状态、会引入新的消息类型、会接入新的调用方。比较合理的做法是,在核心链路上预留统一的幂等处理组件,把“请求指纹生成”“幂等校验”“结果返回”抽象为可复用环节。这样新增业务时只需要注册新的幂等场景,而不需要重新写一套逻辑。
最后再补充一个小技巧:上线幂等机制前,一定要写好灰度方案。我一般是针对新接口先开启幂等校验,观察日志中重复请求占比和业务异常率的曲线,确认稳定后再逐步全量。不要一上来就硬切,否则一旦幂等设计有误,线上会陷入大量请求被“误判为重复”的危机里。
接口幂等就讲到这里。方案也好,代码也好,都不复杂,复杂的是你怎么看待这个问题、怎么为极端情况留好退路。希望这篇实战拆解能帮你少踩几个坑。