当前位置: 首页 > news >正文

接口幂等性设计与实战:支付、下单、重试场景怎么搞?

前言

用户点一次「提交订单」可能因为网络抖动、前端重复点击、网关重试而变成多次请求。如果接口不幂等,就会重复扣款、重复下单、重复发券。支付、下单、退款、发放权益这类接口,幂等是必选项,不是可选项。

本文从「什么是幂等」讲起,到常见实现方式(唯一键、token、状态机),再落到支付、下单、消息消费等实战场景,并给出 Java 示例和踩坑提醒,方便直接用到项目里。


1. 什么是幂等?

幂等:同一个请求执行一次和执行多次,对资源的影响是一样的;从结果上看,和只执行一次等价。

  • 查询、删除(按ID):天然幂等。查多次、删多次结果一致。
  • 新增、更新、支付、下单:不设计就不幂等。重复调用会多扣钱、多下单。

所以幂等设计主要针对「会改变状态、会产生副作用的写操作」。


2. 为什么会有重复请求?

来源说明
用户/前端连续点击、刷新提交、返回后再次提交
网络/网关超时重试、负载均衡重试、HTTP 重试
消息队列至少一次语义下的重复投递、消费失败重试
定时任务重复触发、多实例同时跑

只要存在重试或重复调用,就要在业务层保证幂等,不能只依赖「只调一次」。


3. 常见实现思路

3.1 唯一键 + 数据库约束(最常用)

业务生成一个唯一请求号/幂等键(订单号、支付流水号、out_trade_no 等),写入时以该字段建唯一索引。第一次插入成功,后续相同键再插入会唯一冲突,直接视为重复请求,返回「已处理」或原结果。

优点:实现简单、可靠、不依赖外部存储。
缺点:依赖 DB 唯一约束;键要业务自己能生成且唯一(雪花、号段、UUID 等)。

-- 订单表CREATETABLEorders(idBIGINTPRIMARYKEY,order_noVARCHAR(64)NOTNULLUNIQUECOMMENT'订单号,幂等键',user_idBIGINTNOTNULL,amountDECIMAL(10,2),statusINT,created_atDATETIME);
// 伪代码publicOrdercreateOrder(CreateOrderRequestreq){StringorderNo=req.getOrderNo();// 前端或上游传入唯一订单号try{returnorderDao.insert(Order.builder().orderNo(orderNo).userId(req.getUserId())...build());}catch(DuplicateKeyExceptione){returnorderDao.getByOrderNo(orderNo);// 重复请求,返回原订单}}

3.2 幂等 token(防重复提交)

流程:先向服务端申请一个一次性幂等 token,提交时把 token 带上;服务端校验 token 存在则执行业务并删除(或标记使用),同一 token 再次请求则拒绝。

优点:适合前端「防重复点击」、无唯一业务单号时的表单提交。
缺点:需要存 token(Redis/DB),有过期和清理问题;高并发时要注意「校验-删除」的原子性。

// 1. 申请 token(可放在打开页面或点击「提交」时调一次)Stringtoken=UUID.randomUUID().toString();redis.set("idempotent:order:"+token,"1",5,MINUTES);// 2. 提交时带 tokenpublicOrdercreateOrder(StringidempotentToken,CreateOrderRequestreq){Stringkey="idempotent:order:"+idempotentToken;if(!redis.delete(key)){// 删除成功才继续,保证只用一次thrownewBizException("请勿重复提交");}returndoCreateOrder(req);}

注意:get + delete非原子,高并发下可能重复;Redis 可用 Lua 或「SET key 1 NX EX 5」配合删除,或直接用「存在则删除并返回是否删除成功」的语义。

3.3 状态机(防重复推进)

订单/支付单有明确状态(待支付→已支付→已发货…)。只有当前状态允许才执行操作,并在一次更新中「校验旧状态 + 更新新状态」。同一请求多次调用,第一次把状态改掉后,后续校验旧状态失败,不会重复推进。

优点:和业务强绑定,避免重复支付、重复发货等。
缺点:要设计清晰的状态和流转;更新时要带条件(如WHERE status = 待支付),并判断影响行数。

-- 只有「待支付」才能变成「已支付」UPDATEordersSETstatus='已支付',pay_time=NOW()WHEREorder_no=?ANDstatus='待支付';-- 若 affected rows = 0,说明已支付或已取消,直接返回幂等结果
intn=orderDao.updateStatusToPaid(orderNo);if(n==0){Orderexisting=orderDao.getByOrderNo(orderNo);if(existing.getStatus().equals("已支付")){returnexisting;// 幂等:已处理过}thrownewBizException("订单状态不允许支付");}

3.4 分布式锁

对「同一业务键」加锁,例如 RedisSET key NX EX,拿到锁的请求执行业务,执行完释放;同一键的并发请求只有一个能拿到锁。
注意:锁只能串行化并发,不能区分「同一次请求重试」和「另一次新请求」。通常和唯一键或 token 配合:先保证「同一请求」用同一个幂等键,再用锁保护「校验+执行」的原子性(可选)。


4. 场景一:支付回调/支付结果同步

支付渠道(微信/支付宝)可能多次回调或推送,必须幂等。

做法

  • 支付渠道的支付单号(或 out_trade_no + 渠道)为幂等键,建唯一索引。
  • 回调里:先按支付单号查本地是否已有「已支付」记录;没有则插入支付流水(唯一键),再更新订单状态(用状态机:仅当待支付→已支付)。
  • 插入唯一键冲突或状态机更新影响行数为 0,都视为重复,返回 success,避免渠道反复重试。
@TransactionalpublicvoidonPayNotify(PayNotifyRequestreq){StringpayOrderNo=req.getPayOrderNo();if(payRecordDao.existsByPayOrderNo(payOrderNo)){return;// 已处理,直接返回成功}payRecordDao.insert(PayRecord.builder().payOrderNo(payOrderNo)...build());orderDao.updateStatusToPaid(req.getOrderNo());}

5. 场景二:下单接口

做法

  • 前端或网关生成订单号(或用后端生成的唯一 requestId),下单请求带 orderNo。
  • 表上 order_no 唯一索引;insert 成功即创建,DuplicateKey 则 select 返回原订单(同一订单号视为重复下单)。

若不想让前端传订单号,可以用「用户 + 商品 + 短时间窗口」生成幂等键,或先生成订单号再调下单(先拿号、再提交)。


6. 场景三:消息队列消费

MQ 至少一次语义下,同一条消息可能被投递多次,消费端必须幂等。

做法

  • 消息业务主键(如订单号、支付单号、消息ID)为幂等键,在 DB 或 Redis 记录「已处理」。
  • 先查是否已处理;未处理则执行业务并写入「已处理」;已处理则直接 ack,不重复执行业务。
publicvoidconsume(Messagemsg){StringidempotentKey=msg.getOrderNo();if(redis.setNX("consumed:"+idempotentKey,"1",24,HOURS)){try{doCreateOrder(msg);}finally{// 可延长 key 或落库,防止 MQ 重试时 key 已过期}}// 已处理过,直接 ack}

7. 场景四:退款、取消、发放权益

思路一致:

  • 退款:以退款单号或「订单号+退款批次」为幂等键,唯一表 + 状态机(仅待退款→退款中/已退款)。
  • 取消订单:订单号 + 状态机(仅待支付/待发货→已取消)。
  • 发券/发积分:以「用户+活动+发放批次」或流水号做唯一键,重复则跳过。

8. 实战注意点

8.1 幂等键谁生成?

  • 订单号、支付单号:建议服务端生成(雪花/号段),前端只传「创建订单」意图;或前端传 requestId,服务端用 requestId 做幂等键。
  • 支付回调:以渠道的支付单号为准,不要只用自家订单号(同一订单可能多次支付尝试)。

8.2 返回什么?

重复请求时,建议返回与第一次一致的结果(如订单详情、支付结果),并 HTTP 200,这样前端和渠道都按成功处理,不会继续重试。不要返回 4xx 导致调用方误以为失败而重试。

8.3 过期与清理

  • token 幂等:设置合理过期时间,并避免 key 无限增长。
  • 流水表:可按时间分表或归档,唯一键在有效期内即可。

8.4 事务与顺序

幂等校验(如查是否已存在)和业务写要在同一事务里,避免并发下重复通过校验。状态机更新用「WHERE 旧状态」并检查影响行数,保证只推进一次。


9. 总结

方式适用场景要点
唯一键 + 约束下单、支付流水、退款单业务生成唯一号,DB 唯一索引,冲突即重复
幂等 token前端防重复提交、无业务单号一次性 token,用完即删,注意原子性
状态机支付、退款、取消、发货仅允许当前状态→下一状态,WHERE 条件 + 影响行数
分布式锁配合上述使用串行化「校验+执行」,不单独作为幂等方案

原则:会改状态、会扣钱、会发权益的接口,都设计成幂等;幂等键选好、状态机画清楚、重复请求返回首次结果,就能覆盖绝大多数后端场景。

http://www.jsqmd.com/news/408654/

相关文章:

  • 2026企业知识库部署服务商推荐:实力方案商、本地部署、安全合规厂商全汇总 - 品牌2025
  • 题解:P14765 [ICPC 2024 Seoul R] Bottles
  • 2026年15万左右城市SUV终极评测(权威机构双重背书)| 家庭选车避坑全指南 - 十大品牌推荐
  • 你好,芯片设计新搭档:用LLM自动化RTL设计的正确姿势
  • 计算机毕业设计之springboot基于小程序社区垃圾分类管理系统
  • 2026国内最新培育钻生产厂家top5推荐!广东广州等地优质培育钻公司权威榜单发布,环保质感兼具的时尚钻饰之选 - 十大品牌榜
  • 春晚之后机器人遭“疯抢”,具身下半场锁定商业化
  • 计算机毕业设计之thinkphp5可可美甲管理系统
  • 【开题答辩全过程】以 哈尔滨高校职称评审投票系统为例,包含答辩的问题和答案
  • 2026企业知识库部署厂商推荐:私有化、本地化、行业定制方案全覆盖 - 品牌2025
  • 计算机毕业设计之springboot母婴类购物系统
  • 2026国内最新18k饰品供应链top10推荐!广东广州等地优质18k饰品厂家权威榜单发布,工艺品质双优助力时尚选购 - 十大品牌榜
  • 【开题答辩全过程】以 红色博物馆预约系统为例,包含答辩的问题和答案
  • AI智能体与知识变现的心理机制:从认知偏差到信任构建|创客匠人
  • 2003-2024年地级市数字人才、信息技术人才数据
  • 知识变现的可持续进化:从单点爆款到智能体驱动的生态系统|创客匠人
  • 干货!B2B制造业出海,推荐几家专业的海外独立站建站与Google代运营服务商 - 品牌2025
  • 【开题答辩全过程】以 会计管理系统的设计与实现为例,包含答辩的问题和答案
  • 2026年自动驾驶数据标注厂家最新推荐:成都数据标注公司/数据标注接单平台/数据标注的企业/数据标注管理平台/选择指南 - 优质品牌商家
  • 移动App开发如何用 XinServer 降低后台维护成本?
  • 家长口碑之选:靠谱的青少儿英语培训班推荐 - 品牌2025
  • 2026三边封拉链袋哪家强?实测推荐优质厂商,中封袋/包装袋/自立袋/四边封包装袋/自立拉链袋,三边封拉链袋制造厂推荐 - 品牌推荐师
  • 海南大学徐顺清课题组在《Environmental Science Technology》发表新成果:多卤代咔唑的神经毒性机制研究
  • 2026年全国加气站设备厂家哪家权威?可靠优质实力厂家及选型参考 - 深度智识库
  • 2026年优质GEO服务商,DeepSeek GEO专属适配,出海优选 - 品牌2025
  • D5 Render 3.0 与 AI 时代:建筑可视化工作流的全面重构
  • THUPC2026 初赛游记
  • 2026年2月亲测复盘:三家口碑塑料造粒机工厂的实测三维测评与选型,拆解三大工厂的“内核”逻辑! - 品牌推荐用户报道者
  • 2026年2月实测复盘:口碑最好的PE钢骨架复合管公司,三维测评多场景下的性能数据! - 品牌推荐用户报道者
  • 2026 少儿英语线下培训优选:口碑好、体验佳的机构推荐 - 品牌2025