当前位置:首页 > 云谷精选

幂等设计在防止重复处理时有哪些通用方案

admin2026年03月18日云谷精选2.24万
摘要:# 幂等设计:别再让用户“多敲一次回车”就出事了 前两天,我帮朋友排查一个线上问题。挺简单的场景:用户提交订单,网络有点卡,他下意识地多点了两下“支付”按钮。结果你猜怎么着?系统真给他生成了**两笔一模一样的订单**,钱也扣了两次。用户炸了,客服忙了,开…

幂等设计:别再让用户“多敲一次回车”就出事了

前两天,我帮朋友排查一个线上问题。挺简单的场景:用户提交订单,网络有点卡,他下意识地多点了两下“支付”按钮。结果你猜怎么着?系统真给他生成了两笔一模一样的订单,钱也扣了两次。用户炸了,客服忙了,开发半夜爬起来改bug了。

这种场景你应该不陌生吧?说白了,这就是典型的“重复请求”问题。在分布式系统里,网络抖动、前端防抖失效、用户手速太快……任何一点小波动,都可能让同一个请求被处理多次。

幂等设计,就是解决这个问题的“金钟罩”。它不是一个多么高大上的概念,核心就一句话:无论同一个操作请求被执行一次还是多次,最终的系统状态都是一致的。

听起来像句正确的废话?但就这“一句废话”,没做好的团队,轻则数据错乱,重则资金损失,真不是闹着玩的。

一、先搞明白:什么时候需要“幂等”?

别一上来就想着上方案。我见过不少团队,不管三七二十一,所有接口都加幂等,搞得系统复杂不堪。其实吧,幂等主要用在有副作用的操作上。

什么叫“副作用”?就是会改变系统状态的操作。比如:

  • 创建类:提交订单、创建用户、生成支付单。
  • 更新类:扣减库存、账户充值、更新状态(比如从“待支付”改成“已支付”)。
  • 删除类:理论上删除一次和删除多次结果一样(都是没了),但也要小心,别把删除日志记录个没完。

而像纯查询(GET请求)、计算这类操作,天生就是幂等的,你查一百遍,数据也不会变,就别瞎折腾了。

很多技术文档一上来就跟你讲原理,但我觉得,从“怎么用”反推“为什么”,反而更容易理解。下面这几个方案,就是实战中最常见的“防重”手段。

二、通用方案盘点:从“土办法”到“优雅解”

方案1:Token令牌机制(前端防重的主流选择)

这可能是你最熟悉,也最容易理解的一种。

流程是这样的:

  1. 用户打开表单(比如支付页面),前端先向后端申请一个唯一的“令牌”(Token)。
  2. 后端生成这个Token(通常用UUID就行),存到Redis里,设置个短时间有效期(比如5分钟),然后返回给前端。
  3. 前端提交业务请求时,把这个Token一起带到请求参数里。
  4. 后端收到请求,先不去处理业务逻辑,而是去Redis里查这个Token是否存在。
    • 如果存在:说明是第一次请求。删除这个Token(确保用完后失效),然后执行业务。
    • 如果不存在:说明Token已经被用过了(或者过期了),直接拒绝这次请求,返回“请勿重复提交”之类的提示。

说白了,这就是个“一次性的门票”。 你手里有票才能进门,检票员(后端)撕了你的票,后面的人就算拿着同样的票根(重复请求)也进不来了。

优点:理解成本低,实现简单,对前端友好。 缺点:需要前后端配合,多一次获取Token的交互。而且,它防的是“短时间内的重复提交”,对于异步消息、定时任务重试这种场景,就不太适用了。

方案2:数据库唯一约束(简单粗暴,但有效)

这是最古老也最可靠的方法之一,尤其适合创建类的业务。

举个例子:用户注册,用户名不能重复。你怎么保证? 很简单,在数据库里给username字段加个唯一索引。当第一个插入请求成功,第二个重复的请求再来时,数据库直接会报Duplicate entry错误,插入失败。业务层捕获这个异常,返回“用户名已存在”即可。

不只是业务字段,我们还可以引入一个“幂等号”。 比如,在订单表里加一个idempotent_key字段,并设为唯一索引。这个key可以是前端生成的UUID,也可以是拼接业务字段(比如“支付_用户ID_时间戳”)。插入订单前,先尝试插入这个幂等key。成功,则继续;失败,则说明是重复请求,直接查询已创建的订单返回。

优点:数据库层面保证绝对唯一,可靠性极高,实现成本低。 缺点只适用于插入操作。而且,数据库报错属于“异常流”,用异常来控制业务逻辑,一些追求优雅的架构师会有点嫌弃。另外,如果分库分表,唯一索引的设计会复杂一些。

方案3:状态机流转(业务逻辑自带的防错)

这是我最推崇的一种方式,因为它把防重逻辑和业务状态完美地融合在了一起,非常自然。

很多业务对象本身就有明确的生命周期状态。比如订单:待支付 -> 已支付 -> 已发货 -> 已完成。一个订单从待支付已支付,理论上只能发生一次。

那么,在支付回调接口里,你完全可以这样写:

UPDATE order SET status = '已支付' WHERE order_id = '123' AND status = '待支付';

看看这个SQL的WHERE条件。如果这条订单已经支付过了,status已经不是待支付了,那么这次更新语句的影响行数就是0。后端根据影响行数,就能判断出这是不是一次重复的支付回调。

这招真绝了。 它不需要引入任何额外的中间件或字段,纯粹利用业务状态本身来保证幂等。代码清晰,逻辑自洽。

优点:天然契合业务,无额外开销,代码简洁。 缺点:要求业务必须有清晰、稳定的状态流转设计。如果业务逻辑乱七八糟,状态可以随意跳转,这招就废了。

方案4:分布式锁(应对并发场景的“大锤”)

上面几种方案,在真正的高并发、分布式环境下,可能会遇到一点挑战:在“判断”和“执行”两个动作之间,仍然有一个极短的时间窗口,可能被另一个请求插足。

这时候,就需要请出“大锤”——分布式锁。它的思路很简单:在执行业务前,先抢一把锁,抢到了才让你进去执行,执行完再释放锁。

具体到幂等:

  1. 以业务的唯一标识(比如订单号order_id)作为锁的Key。
  2. 请求进来,先尝试获取这个锁(SET order_id NX PX 3000,NX表示不存在才设置,PX设置超时时间防止死锁)。
  3. 如果获取成功,说明是第一个请求,执行业务,完成后释放锁(或等待自动过期)。
  4. 如果获取失败,说明已经有请求在处理了,直接返回“处理中”或查询已有结果。

听起来很完美?但它有个致命缺点:太重了。 每次请求都要走一遍获取锁的流程,对Redis等锁服务造成压力。而且,锁的粒度、超时时间设置不好,很容易引发新的问题(比如锁过期了业务还没执行完)。

所以我的建议是:把它当成最后的防线。 优先用前面更轻量的方案,只有在极端高并发、且业务逻辑本身非常复杂(判断重复需要查多处数据)的场景下,再考虑加锁。

三、怎么选?一张图给你理清思路

光讲方案不行,得能落地。我画了张简单的决策图(当然是在脑子里,你凑合看):

新请求来了
    |
    v
是“创建”或“更新状态”操作吗? ——否——> 可能不需要强幂等
    |
    v
是
    |
    v
前端能否方便控制(如表单)? ——是——> 【方案1:Token令牌】,用户体验好
    |
    v
否
    |
    v
业务是否有清晰状态流转? ——是——> 【方案3:状态机】,最优雅
    |
    v
否
    |
    v
能否在数据库层做唯一约束? ——是——> 【方案2:唯一约束】,最可靠
    |
    v
否(或并发极高,需绝对互斥)
    |
    v
【方案4:分布式锁】,慎用,评估好性能

四、几个容易踩的坑(血泪经验)

  1. “幂等”不是“防并发”:这是最常见的误解。幂等保证的是结果正确,但不保证请求的处理顺序。100个重复支付请求,用状态机方案,最后只有一个成功,这叫幂等。但它们可能几乎同时到达,你的服务要能扛住这瞬间的并发,这需要另外的限流、熔断措施。
  2. “已失效”的Token:Token机制中,如果业务执行成功了,但删除Redis中Token的步骤失败了(网络问题),那么这个Token就“泄露”了,永远占着位置,导致用户无法再次提交。所以,删除Token的操作一定要有重试和最终兜底(比如靠过期时间)。
  3. “全局唯一”的挑战:自己生成的UUID在理论上并非100%全局唯一(虽然碰撞概率极低)。在金融级场景,有时会要求使用由发号器(如雪花算法)生成的、具备业务意义的全局唯一ID来作为幂等号,可靠性更高。
  4. 别忘了“查询”接口:是的,查询本身是幂等的。但如果你的查询接口会写缓存、记日志,这些“边角料”操作也可能被重复执行,导致缓存被意外更新、日志重复记录。虽然影响可能不大,但设计时心里要有数。

写在最后

幂等设计,本质上是一种防御性编程思维。它承认网络是不可靠的、用户是“手滑”的、系统是可能重试的。然后,在这种“不完美”的现实中,通过一些或简单或精巧的设计,让系统行为变得确定、可靠。

别再把它当成一个面试八股文了。回头看看你的系统,那些核心的创建、支付、状态变更接口,是不是还裸奔在重复请求的风险之下?如果是,你心里其实已经有答案了。

从最简单的数据库唯一索引开始,给它加上一层防护。这活儿不酷,但出了事的时候,它能救你的命。

行了,不废话了,检查代码去吧。

扫描二维码推送至手机访问。

版权声明:本文由www.ysyg.cn发布,如需转载请注明出处。

本文链接:http://www.ysyg.cn:80/?id=387

“幂等设计在防止重复处理时有哪些通用方案” 的相关文章

探究针对QUIC协议的防御挑战:新型UDP加密流量的识别算法

# QUIC协议:当“加密快车”冲垮传统防线,我们该如何设卡? 我得先坦白,这事儿我琢磨了挺久。因为每次跟客户聊起DDoS防护,说到UDP洪水,大家总是一脸“懂了”——直到我补一句:“那要是攻击者用上QUIC协议呢?”会议室里多半会安静几秒,然后有人试探…

基于行为分析的智能WAF算法:过滤SQL注入与命令执行的技术细节

# 别让SQL注入和命令执行“摸”进你家服务器:聊聊行为分析WAF那点事 我前两天帮一个做电商的朋友看服务器日志,好家伙,那攻击请求密密麻麻的,跟春运火车站似的。大部分都是些老掉牙的SQL注入尝试,什么`' OR 1=1 --`,一看就是脚本小子批量扫的…

解析高防 CDN 在保障混合云架构安全性中的流量分发逻辑

# 高防CDN,是怎么给混合云“撑腰”的? 你肯定见过那种场面:业务高峰来了,自家机房(私有云)的服务器吭哧吭哧,眼看要撑不住,赶紧把一部分流量“甩”给公有云去扛。这就是混合云的日常,灵活是真灵活。 但问题也来了——你的业务入口,现在是“多点开花”了。…

详解高防 CDN 应对大规模静态资源盗刷的流量保护与计费对策

# 静态资源被盗刷?高防CDN的“守财”实战手册 做网站运营的,最怕什么?不是黑客正面硬刚,而是那种悄无声息、温水煮青蛙式的**静态资源盗刷**。 我自己就见过一个挺惨的案例。一个做在线教育的朋友,某天早上起来发现云存储的账单直接爆了,费用是平时月费的…

详解如何通过高防 CDN 拦截针对 WordPress 等 CMS 系统的暴力破解

# 别让WordPress后台被“盲猜”到瘫痪,高防CDN这么用才真防得住 我前两天帮朋友处理一个WordPress站点,那场面,真是哭笑不得。他上了个“企业级”防火墙,结果后台登录页面 `/wp-admin` 每天被来自全球的IP轮番“敲门”,CPU直…

探讨高防 CDN 接入后出现 504 Gateway Timeout 的技术排查流程

# 高防CDN一上,网站反而504了?别慌,老司机带你一步步“破案” 我前两天刚帮一个做电商的朋友处理了个棘手的故障。他兴冲冲地接入了某家大厂的高防CDN,想着从此可以高枕无忧,不怕打也不怕卡。结果上线当天,后台就炸了——用户时不时就刷出个**504 G…