🧑 博主简介:CSDN博客专家,「历代文学网」(PC端可以访问:https://lidaiwenxue.com/#/?__c=1000,移动端可关注服务号 “ 心海云图 ” WX小程序搜索“历代文学”)总架构师,首席架构师,也是联合创始人!16年工作经验,精通Java编程,高并发设计,分布式系统架构设计,Springboot和微服务,熟悉Linux,ESXI虚拟化以及云原生Docker和K8s,热衷于探索科技的边界,并将理论知识转化为实际应用。保持对新技术的好奇心,乐于分享所学,希望通过我的实践经历和见解,启发他人的创新思维。在这里,我希望能与志同道合的朋友交流探讨,共同进步,一起在技术的世界里不断学习成长。


电商幂等设计全解析:从下单到支付,如何为系统“防重放”
别让用户的“手抖”和网络的“重试”,变成你系统的“资损”事故
一、引言:什么是幂等?为什么电商离不开它?
在软件开发中,幂等(Idempotence)指的是:一个操作,无论执行一次还是执行多次,所产生的副作用(如数据变更、资金扣减)是完全相同的。
对于电商系统而言,幂等不是“可选项”,而是“必选项”。一次网络抖动、一次按钮误触、一次支付回调的重试,如果系统没有幂等保护,就可能产生两笔重复订单、两次库存扣减、两笔重复扣款——每一个错误都直接指向“资损”或“客诉”。
本文将结合订单创建、支付回调、库存扣减三大核心场景,从原理到代码,详解电商幂等设计的完整方案。
二、一个最常见的认知误区
误解:幂等设计就是“防止用户短时间内下两单”。
正解:幂等设计是**“防止同一个业务请求因网络重发而被重复执行”**。
场景还原:
用户最终只生成 1 笔订单,扣了 1 次库存。
如果用户真的想买两单,他需要手动点击两次“提交”。前端每次点击会生成不同的令牌(如 T-001 和 T-002),系统会分别处理,生成两笔独立订单。幂等不会拦截合法的新请求。
三、核心场景一:订单创建 —— 最经典的幂等战场
1. 方案选型:前端生成 UUID + 后端 Redis 缓存
这是业界最通用、最干净的方案。
- 前端:每次点击提交,调用 crypto.randomUUID() 生成全局唯一 ID,放入请求头(如 Idempotent-Token)。
- 后端:接收请求后,以该 UUID 为 Key,利用 Redis 的 SET NX 命令进行原子性判断:
- Key 不存在 → 设置成功 → 执行业务(创建订单、扣库存)
- Key 已存在 → 直接返回“重复请求”,不执行任何业务
2. 缓存时长设计(黄金法则)
建议值:订单未支付超时时间 + 1~2 分钟。例如订单有效期 30 分钟,则令牌缓存 15~30 分钟。
- 下限:必须覆盖网络重试的最长时间(通常 3~5 分钟)
- 上限:必须短于订单有效期,否则订单超时取消后,用户重新下单会因旧令牌被拦截而无法购买
3. 关于“垃圾 Key”的释怀
一个 UUID Key 约占用 100 字节内存。日均 100 万订单,同时存活的 Key 仅约 2 万个,总内存占用约 2MB。Redis 自带过期删除机制,这笔“保险”成本几乎可以忽略不计。
四、核心场景二:支付回调 —— 直接涉及资金安全的防线
支付幂等的核心是:防止同一笔支付流水被重复处理。
1. 第一道防线:数据库唯一约束
在支付记录表中,为 第三方交易流水号(Transaction ID) 设置唯一索引。
- 第一次回调:插入成功,更新订单状态为“已支付”
- 第二次重复回调:插入时触发唯一键冲突,捕获异常后直接返回成功响应(告知支付平台已处理)
这是最硬核、最可靠的兜底方案。
2. 第二道防线:状态机更新
订单状态是单向流转的:待支付 → 支付中 → 已支付 → 已发货。
更新 SQL 示例:
UPDATE orders
SET status = '已支付'
WHERE order_id = ? AND status = '待支付';
如果订单已经不是“待支付”状态,更新影响行数为 0,程序即可判断“已处理过”,安全返回。
3. 终极保障:日终对账
即使前述环节出现 Bug,每日凌晨的对账系统会自动比对支付平台账单与本地记录,发现差异自动告警或补单,保证最终一致性。
五、核心场景三:秒杀扣库存 —— 高并发下的双重保障
秒杀场景的特点是:流量巨大、竞争激烈。库存幂等需要同时解决两个问题:
- 防超卖:库存不能扣成负数
- 防重复:同一个请求不能重复扣库存
1. 防超卖:乐观锁 SQL
UPDATE goods
SET stock = stock – 1
WHERE id = ? AND stock > 0;
只要库存 > 0 才扣减,确保库存永不为负。
2. 防重复:Redis + Lua 脚本原子操作
Lua 脚本逻辑:
将上述逻辑封装在 Lua 脚本中,利用 Redis 的单线程特性保证原子性。
3. 异步落库 + 对账
秒杀订单先记录在 Redis,后续异步批量写入数据库。即便极端情况下缓存丢失,日终对账仍能发现并修复数据差异。
六、幂等标记的三种来源
并不是所有场景都需要前端传 UUID,根据业务性质,我们可以灵活选择:
| 前端生成 | 下单、秒杀、领券 | crypto.randomUUID() |
| 后端业务组合 | 支付回调、同步通知 | 订单号 + 支付状态 或第三方流水号 |
| 状态机判断 | 取消、收货、退款 | UPDATE … WHERE status = '待支付' |
七、扩展场景:MQ 消费与表单重复提交
- 消息队列消费:MQ 的“至少一次”语义会导致重复消费。消费者必须根据消息中的唯一业务 ID(如订单号)进行幂等判断,或利用数据库唯一键防重。
- 表单重复提交:用户刷新或后退可能导致重复提交。同样适用“前端令牌 + 后端缓存”方案,提交后立即失效。
八、总结:一句口诀 + 一张表
判断原则:如果某个功能重复执行会导致数据重复、资金损失或状态混乱,那么它就必须做幂等设计。
| 创建订单 | 前端 UUID + Redis SET NX | 缓存时长 < 订单有效期 |
| 支付回调 | 第三方流水号唯一索引 | 配合状态机更新 |
| 扣减库存 | 乐观锁 SQL + Redis Lua 脚本 | 防超卖 + 防重复 |
| MQ 消费 | 业务唯一 ID 去重 | 消费方自己保证幂等 |
幂等设计,本质上是用可控的存储成本(如 2MB 的 Redis 内存)去交换不可控的资金风险。这笔投资,对于任何一个电商系统而言,都是最值得的。
最后送大家一句话: 宁可多做一次判断,也不给资损留一丝机会。





