接口幂等性设计:如何防止用户重复提交与并发请求?
当前位置:点晴教程→知识管理交流
→『 技术文档交流 』
本文系统讲解接口幂等性设计的核心方案。从重复提交和并发请求两大痛点出发,覆盖唯一索引、Token机制、乐观锁、分布式锁、状态机五种落地实现,附方案对比表与常见坑位。读完这篇,你能在实际项目里快速选对幂等方案。 你有没有遇到过这种情况。 用户在支付页面连点了两次「确认支付」,结果扣了两次款。客服工单炸了。 或者更隐蔽的。网络超时,客户端自动重试,后端收到两个一模一样的请求,订单创建了两个。用户没感知,后台数据对不上。 这不是 bug。这是没做幂等性设计。 01 幂等性到底是什么 幂等性这个词来自数学。f(f(x)) = f(x),对同一个输入做多次操作,结果跟做一次一样。 放到接口里就是,同一个请求,不管你调一次还是调一百次,对系统状态的影响完全一致。 注意,不是「拒绝重复请求」,而是「重复了也没关系」。这是两种不同的思路,后面会展开讲。 ![]() 幂等性概念示意:重复请求经去重后只处理一次 02 重复请求比你以为的常见 重复请求在生活中比你想的常见得多。 第一类,用户重复提交。网慢、手抖、按钮没灰掉,用户多按了几下。手机信号差的时候尤其严重,你以为没响应再点一次,其实第一次已经发出去了。 第二类,网络重试。HTTP 客户端的超时重试、消息队列的死信重投、网关的重发机制,都可能把同一个请求发过来两三遍。这类问题最隐蔽,测试环境根本复现不了。 第三类,并发请求。用户开了两个浏览器标签,同一秒点了两次「提交订单」。或者抢购场景下,并发量瞬间打上来,同一个用户的请求并发到达。 这三类问题,光靠前端禁用按钮是兜不住的。 前端防的是体验,后端防的才是安全。 03 五种落地方案 下面讲五种主流的幂等实现方案。每种都有适用场景,没有万能解法。 方案一:唯一索引 最简单粗暴的方案。在数据库表上建唯一索引,让数据库帮你挡。 比如订单表,用「业务流水号」做唯一索引。第一个 INSERT 成功,第二个直接报 duplicate key error。代码层面 catch 这个异常,返回「已处理」。 try { 优点:实现成本极低,数据库帮你兜底,最稳的防线。 缺点:只能防「新增」,不防「更新」;依赖业务字段有天然唯一性。 适合场景:创建订单、注册账号、发券等插入型操作。 方案二:Token 机制 前端的表单提交,最经典的做法。 流程是这样的。前端打开页面时,先调一个接口拿一个唯一 Token。提交时把这个 Token 带过去。后端拿到 Token,去 Redis 里删,删成功了就处理请求,删失败说明已经处理过了,直接返回。 ![]() Token 机制流程:获取令牌 → 携带提交 → 原子删除 → 处理或拒绝 String token = request.getHeader("idempotent-token"); 这里有个关键细节。必须用 DEL 命令的返回值判断,而不是先查再删。 先查再删在并发下会有窗口期,两个请求都查到「存在」,然后都删,都通过。Redis 的 DEL 是原子操作,天然解决了这个并发问题。 优点:对业务侵入小,前端加个字段就行。 缺点:需要多一次获取 Token 的请求;Token 过期管理需要考虑。 适合场景:表单提交、支付确认、敏感操作。 方案三:乐观锁 更新操作怎么办?唯一索引防不了 UPDATE。 乐观锁的思路是:每条数据带一个版本号,更新时检查版本号是否变化。如果变了,说明别人已经改过了,你的更新失败。 UPDATE account 如果返回影响行数是 0,说明版本号不匹配,有别人改过了。你需要在业务层做重试或直接返回失败。 优点:不锁表,性能好;天然防并发更新。 缺点:冲突率高时重试成本大;不防重复的新增操作。 适合场景:扣库存、转账、更新配置等更新型操作。 方案四:分布式锁 当业务逻辑复杂,需要一系列操作保证原子性时,单个 SQL 或 Token 搞不定。 用 Redis 的 SETNX 加锁,或者直接用 Redisson。 String lockKey = "order:lock:" + userId; 注意几个坑。锁的 key 要精确到业务维度,别用全局 key。锁要设过期时间,防死锁。过期时间要大于业务执行时间,否则锁提前释放,第二个请求就进来了。 优点:能保护复杂的业务逻辑组合,不限于单条 SQL。 缺点:引入了 Redis 依赖;锁的续期和释放需要小心处理。 适合场景:下单流程(减库存 + 创建订单 + 扣余额)、秒杀、抢红包。 方案五:状态机幂等 有些业务天然有状态流转。订单从「待支付」到「已支付」到「已发货」。 利用状态机做幂等很简单:只允许合法的状态转换,非法的拒绝。 UPDATE orders 如果订单已经是 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 编辑过 |
关键字查询
相关文章
正在查询... |