LOGO 首页 OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 技术文档 其他文档  
 
网站管理员

接口幂等性设计:如何防止用户重复提交与并发请求?

admin
2026年10月21日 0:57 本文热度 131

本文系统讲解接口幂等性设计的核心方案。从重复提交和并发请求两大痛点出发,覆盖唯一索引、Token机制、乐观锁、分布式锁、状态机五种落地实现,附方案对比表与常见坑位。读完这篇,你能在实际项目里快速选对幂等方案。

你有没有遇到过这种情况。

用户在支付页面连点了两次「确认支付」,结果扣了两次款。客服工单炸了。

或者更隐蔽的。网络超时,客户端自动重试,后端收到两个一模一样的请求,订单创建了两个。用户没感知,后台数据对不上。

这不是 bug。这是没做幂等性设计。

01

幂等性到底是什么

幂等性这个词来自数学。f(f(x)) = f(x),对同一个输入做多次操作,结果跟做一次一样。

放到接口里就是,同一个请求,不管你调一次还是调一百次,对系统状态的影响完全一致。

注意,不是「拒绝重复请求」,而是「重复了也没关系」。这是两种不同的思路,后面会展开讲。

幂等性概念示意:重复请求经去重后只处理一次

02

重复请求比你以为的常见

重复请求在生活中比你想的常见得多。

第一类,用户重复提交。网慢、手抖、按钮没灰掉,用户多按了几下。手机信号差的时候尤其严重,你以为没响应再点一次,其实第一次已经发出去了。

第二类,网络重试。HTTP 客户端的超时重试、消息队列的死信重投、网关的重发机制,都可能把同一个请求发过来两三遍。这类问题最隐蔽,测试环境根本复现不了。

第三类,并发请求。用户开了两个浏览器标签,同一秒点了两次「提交订单」。或者抢购场景下,并发量瞬间打上来,同一个用户的请求并发到达。

这三类问题,光靠前端禁用按钮是兜不住的。

前端防的是体验,后端防的才是安全。

03

五种落地方案

下面讲五种主流的幂等实现方案。每种都有适用场景,没有万能解法。

方案一:唯一索引

最简单粗暴的方案。在数据库表上建唯一索引,让数据库帮你挡。

比如订单表,用「业务流水号」做唯一索引。第一个 INSERT 成功,第二个直接报 duplicate key error。代码层面 catch 这个异常,返回「已处理」。

try {
    INSERT INTO orders (biz_no, amount, ...) VALUES (?, ?, ...);
} catch (DuplicateKeyException e) {
    return "订单已存在,请勿重复提交";
}

优点:实现成本极低,数据库帮你兜底,最稳的防线。

缺点:只能防「新增」,不防「更新」;依赖业务字段有天然唯一性。

适合场景:创建订单、注册账号、发券等插入型操作。

方案二:Token 机制

前端的表单提交,最经典的做法。

流程是这样的。前端打开页面时,先调一个接口拿一个唯一 Token。提交时把这个 Token 带过去。后端拿到 Token,去 Redis 里删,删成功了就处理请求,删失败说明已经处理过了,直接返回。

Token 机制流程:获取令牌 → 携带提交 → 原子删除 → 处理或拒绝

String token = request.getHeader("idempotent-token");
Boolean deleted = redis.del(token);
if (!deleted) {
    return "请勿重复提交";
}
// 正常处理业务

这里有个关键细节。必须用 DEL 命令的返回值判断,而不是先查再删。

先查再删在并发下会有窗口期,两个请求都查到「存在」,然后都删,都通过。Redis 的 DEL 是原子操作,天然解决了这个并发问题。

优点:对业务侵入小,前端加个字段就行。

缺点:需要多一次获取 Token 的请求;Token 过期管理需要考虑。

适合场景:表单提交、支付确认、敏感操作。

方案三:乐观锁

更新操作怎么办?唯一索引防不了 UPDATE。

乐观锁的思路是:每条数据带一个版本号,更新时检查版本号是否变化。如果变了,说明别人已经改过了,你的更新失败。

UPDATE account 
SET balance = balance - 100, version = version + 1
WHERE id = ? AND version = ?;

如果返回影响行数是 0,说明版本号不匹配,有别人改过了。你需要在业务层做重试或直接返回失败。

优点:不锁表,性能好;天然防并发更新。

缺点:冲突率高时重试成本大;不防重复的新增操作。

适合场景:扣库存、转账、更新配置等更新型操作。

方案四:分布式锁

当业务逻辑复杂,需要一系列操作保证原子性时,单个 SQL 或 Token 搞不定。

用 Redis 的 SETNX 加锁,或者直接用 Redisson。

String lockKey = "order:lock:" + userId;
RLock lock = redisson.getLock(lockKey);
boolean acquired = lock.tryLock(3, 10, TimeUnit.SECONDS);
if (!acquired) {
    return "操作太频繁,请稍后重试";
}
try {
    // 查状态、判业务、写数据
} finally {
    lock.unlock();
}

注意几个坑。锁的 key 要精确到业务维度,别用全局 key。锁要设过期时间,防死锁。过期时间要大于业务执行时间,否则锁提前释放,第二个请求就进来了。

优点:能保护复杂的业务逻辑组合,不限于单条 SQL。

缺点:引入了 Redis 依赖;锁的续期和释放需要小心处理。

适合场景:下单流程(减库存 + 创建订单 + 扣余额)、秒杀、抢红包。

方案五:状态机幂等

有些业务天然有状态流转。订单从「待支付」到「已支付」到「已发货」。

利用状态机做幂等很简单:只允许合法的状态转换,非法的拒绝。

UPDATE orders 
SET status = 'PAID', paid_at = NOW()
WHERE order_id = ? AND status = 'PENDING';

如果订单已经是 PAID 了,这条 SQL 影响行数是 0,不会重复扣款。跟乐观锁很像,但维度是业务状态而不是版本号。

优点:语义清晰,天然符合业务流程;不需要额外组件。

缺点:只适合有明确状态流转的业务。

适合场景:订单流转、审批流、支付回调。

五种方案各有侧重,关键看你的操作类型

04

方案对比一图看清


​05

几个容易踩的坑

坑一:Token 先删后验还是先验后删?

必须先验后删,而且用 DEL 的返回值判断。先删后验在并发下两个请求都能查到「存在」,都通过。先验后删并且用 DEL 返回值,Redis 的原子性保证了只有一个请求能删成功。

坑二:分布式锁的续期问题。

如果你的业务执行 10 秒,锁 5 秒就过期了,第二个请求拿到锁进来,两个请求同时在跑。Redisson 的看门狗机制可以自动续期,但前提是你用的是不带 leaseTime 参数的 tryLock。

坑三:乐观锁的 ABA 问题。

版本号从 1 变到 2 再变回 1,中间发生了两次修改。用递增的 version 可以避免,因为 version 只增不减。但如果你用「先查再改」,中间窗口期别人改了你不知道。

坑四:唯一索引不是万能的。

它只能防「同一个业务流水号的新增」。如果业务逻辑里有「先查再插」,在高并发下两个请求都查到「不存在」,然后都去插,一个成功一个失败。这时候唯一索引确实是兜底,但你得正确处理这个异常,不能直接抛 500。

06

一句话总结

幂等性不是一种技术,是一种设计纪律。

每种方案都不是万能的,关键是认清你的操作类型。是新增还是更新,是简单还是复杂,有没有天然唯一字段,有没有状态流转。

选对了方案,代码简洁,系统安全。选错了,要么过度设计,要么防不住。

前端防体验,后端防安全。幂等设计永远在后端做兜底。


阅读原文:原文链接


该文章在 2026/10/21 11:25:04 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2026 ClickSun All Rights Reserved  粤ICP备13012886号-2  粤公网安备44030602007207号