接口幂等怎么设计?一次讲清重复提交、支付回调、幂等键与防重落地方案

接口幂等怎么设计?一次讲清重复提交、支付回调、幂等键与防重落地方案

大家好,我是一名有 4 年工作经验的 Java 后端开发。

很多项目都会遇到"同一个请求为什么被执行了两次"这种问题,尤其是下单、支付、发券、库存扣减这些场景,一旦重复执行,后果往往都不小。

这篇文章我想系统聊一聊接口幂等到底该怎么做,什么时候该用幂等号,什么时候该用唯一索引,什么时候要和业务状态一起设计。

🦅个人主页

🐼

文章目录


一、前言

很多人第一次理解接口幂等,通常停留在:

  • 按钮别让用户连点
  • 前端加个防抖

这些当然有帮助,但都不是根本解决方案。

因为真实线上重复请求的来源有很多:

  • 用户重复点击
  • 前端重试
  • 网关重试
  • 下游超时后调用方补发
  • 支付平台重复通知
  • 消息重复消费后再次回调接口

所以真正的问题不是"按钮点了几次",而是:

同一个业务请求重复到达后,系统能不能保证结果只生效一次。

这篇文章就把接口幂等的常见做法和适用场景拆开讲。


二、哪些场景最需要幂等

最典型的高风险场景包括:

  • 创建订单
  • 支付回调
  • 发放优惠券
  • 账户加余额
  • 库存扣减
  • 人工审批
  • 导入批处理触发

这类动作一旦重复执行,可能会导致:

  • 重复下单
  • 重复加钱
  • 重复发券
  • 重复扣库存
  • 重复发货

三、幂等的本质到底是什么

幂等的核心不是:

  • 请求只来一次

而是:

请求即使来了多次,最终业务结果也和执行一次保持一致。

这意味着你不能只想着拦请求,而是要设计:

  • 业务唯一键
  • 去重规则
  • 状态流转约束

四、常见方案怎么选

4.1 前端防抖 / 按钮置灰

优点:

  • 简单
  • 用户体验上能减少重复点击

缺点:

  • 不是安全方案
  • 拦不住重试和回调

4.2 Token / 幂等号

适合:

  • 下单接口
  • 表单提交接口

思路:

  • 先申请一个幂等号
  • 请求时带上
  • 后端只允许这个幂等号成功一次

4.3 唯一索引

适合:

  • 一人一券
  • 一单一记录
  • 一次支付只记一条流水

优点:

  • 最终兜底最强

4.4 状态机限制

适合:

  • 支付状态流转
  • 订单发货
  • 审批状态

例如:

  • 只允许 WAIT_PAY -> PAID

如果已经是 PAID,再来一次支付回调,直接忽略。

4.5 Redis 幂等键

适合:

  • 短时防重
  • 高并发前置拦截

但注意:

  • 更适合第一层挡板
  • 最终仍然要靠数据库或状态机兜底

五、推荐落地方式

大多数核心业务我更推荐:

业务唯一键 + 状态机 + 数据库唯一约束 + 必要时 Redis 前置防重

也就是说:

  • Redis 挡高并发
  • 状态机限制非法重复流转
  • 数据库唯一索引做最终底线

六、代码示例

6.1 用幂等号防止重复下单

java 复制代码
public void createOrder(CreateOrderRequest request) {
    String key = "idempotent:createOrder:" + request.getRequestNo();
    Boolean success = stringRedisTemplate.opsForValue()
            .setIfAbsent(key, "1", Duration.ofMinutes(10));
    if (!Boolean.TRUE.equals(success)) {
        throw new RuntimeException("请勿重复提交");
    }

    orderService.create(request);
}

6.2 用唯一索引防止重复发券

sql 复制代码
CREATE TABLE user_coupon (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id BIGINT NOT NULL,
    coupon_batch_id BIGINT NOT NULL,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uk_user_coupon (user_id, coupon_batch_id)
);

6.3 用状态流转防止重复支付回调

sql 复制代码
update order_info
set status = 'PAID'
where order_id = #{orderId}
  and status = 'WAIT_PAY'

如果影响行数为 0,说明这笔订单已经处理过,不应该再重复执行后续逻辑。


七、最容易踩的坑

7.1 只做前端防重

拦不住真实重复请求。

7.2 只用 Redis,不做最终兜底

Redis 可以过期、可以抖动,不适合单独做最终真相来源。

7.3 只拦请求,不限制状态流转

支付、发货、审批这种场景,状态流转本身就是天然幂等控制点。

7.4 业务唯一键设计错

比如应该用:

  • orderId

结果用了:

  • msgId

就可能挡不住真正的重复业务。


八、面试中怎么回答

如果面试官问你:

接口幂等一般怎么做?

你可以这样回答:

第一,接口幂等的核心不是只防用户重复点击,而是让同一笔业务请求即使重复到达,最终结果也只生效一次。

第二,不同场景会用不同手段。表单提交这类接口适合用幂等号或 token;像发券、流水记录这种天然唯一业务,适合用数据库唯一索引;支付回调、发货、审批这类更适合配合状态机或条件更新来限制状态流转。

第三,在高并发场景里,Redis 可以作为前置防重层,但最终还是建议用数据库唯一约束或状态流转做最后兜底。


九、总结

幂等不是某一个技术点,而是一种业务结果设计。

如果只记一句结论,我觉得可以记住这句:

真正靠谱的幂等设计,通常不是只靠 Redis 或前端防抖,而是"业务唯一键 + 状态流转 + 数据库兜底"一起工作。


十、结尾

如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、关注。

后面我会继续整理一些更偏实战的 Java 后端和电商系统设计文章。

相关推荐
YDS8291 小时前
大营销平台 —— 模板方法串联前中置抽奖规则
java·spring boot·ddd
.柒宇.1 小时前
Java八股之== 与 equals 区别
java·开发语言
时间静止不是简史1 小时前
当MyBatis-Plus的like遇上SQL通配符:下划线翻车记
java·sql·mybatis
喜欢流萤吖~1 小时前
SpringBoot 性能优化实战
spring boot·后端·性能优化
两年半的个人练习生^_^2 小时前
每日一学:设计模式之建造者模式
java·开发语言·设计模式
我登哥MVP2 小时前
【SpringMVC笔记】 - 6 - RESTFul编程风格
java·spring boot·spring·servlet·tomcat·maven·restful
yhole2 小时前
spring security 超详细使用教程(接入springboot、前后端分离)
java·spring boot·spring
zjjsctcdl2 小时前
SpringBoot3.3.0集成Knife4j4.5.0实战
java
常利兵2 小时前
Spring Boot 搭建邮件发送系统:开启你的邮件自动化之旅
spring boot·后端·自动化