接口幂等怎么设计?一次讲清重复提交、支付回调、幂等键与防重落地方案
大家好,我是一名有 4 年工作经验的 Java 后端开发。
很多项目都会遇到"同一个请求为什么被执行了两次"这种问题,尤其是下单、支付、发券、库存扣减这些场景,一旦重复执行,后果往往都不小。
这篇文章我想系统聊一聊接口幂等到底该怎么做,什么时候该用幂等号,什么时候该用唯一索引,什么时候要和业务状态一起设计。
🦅个人主页
🐼
文章目录
- 接口幂等怎么设计?一次讲清重复提交、支付回调、幂等键与防重落地方案
-
- 一、前言
- 二、哪些场景最需要幂等
- 三、幂等的本质到底是什么
- 四、常见方案怎么选
-
- [4.1 前端防抖 / 按钮置灰](#4.1 前端防抖 / 按钮置灰)
- [4.2 Token / 幂等号](#4.2 Token / 幂等号)
- [4.3 唯一索引](#4.3 唯一索引)
- [4.4 状态机限制](#4.4 状态机限制)
- [4.5 Redis 幂等键](#4.5 Redis 幂等键)
- 五、推荐落地方式
- 六、代码示例
-
- [6.1 用幂等号防止重复下单](#6.1 用幂等号防止重复下单)
- [6.2 用唯一索引防止重复发券](#6.2 用唯一索引防止重复发券)
- [6.3 用状态流转防止重复支付回调](#6.3 用状态流转防止重复支付回调)
- 七、最容易踩的坑
-
- [7.1 只做前端防重](#7.1 只做前端防重)
- [7.2 只用 Redis,不做最终兜底](#7.2 只用 Redis,不做最终兜底)
- [7.3 只拦请求,不限制状态流转](#7.3 只拦请求,不限制状态流转)
- [7.4 业务唯一键设计错](#7.4 业务唯一键设计错)
- 八、面试中怎么回答
- 九、总结
- 十、结尾
一、前言
很多人第一次理解接口幂等,通常停留在:
- 按钮别让用户连点
- 前端加个防抖
这些当然有帮助,但都不是根本解决方案。
因为真实线上重复请求的来源有很多:
- 用户重复点击
- 前端重试
- 网关重试
- 下游超时后调用方补发
- 支付平台重复通知
- 消息重复消费后再次回调接口
所以真正的问题不是"按钮点了几次",而是:
同一个业务请求重复到达后,系统能不能保证结果只生效一次。
这篇文章就把接口幂等的常见做法和适用场景拆开讲。
二、哪些场景最需要幂等
最典型的高风险场景包括:
- 创建订单
- 支付回调
- 发放优惠券
- 账户加余额
- 库存扣减
- 人工审批
- 导入批处理触发
这类动作一旦重复执行,可能会导致:
- 重复下单
- 重复加钱
- 重复发券
- 重复扣库存
- 重复发货
三、幂等的本质到底是什么
幂等的核心不是:
- 请求只来一次
而是:
请求即使来了多次,最终业务结果也和执行一次保持一致。
这意味着你不能只想着拦请求,而是要设计:
- 业务唯一键
- 去重规则
- 状态流转约束
四、常见方案怎么选
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 后端和电商系统设计文章。