前言
你可能见过这类线上事故:用户点了一次"支付",后台却收到两次请求,结果真的扣了两次。
很多人第一反应是"前端点太快了",但工程上真正要解决的是:重复请求来了,你的系统还能不能只生效一次。这就是幂等性要处理的问题。
这篇文章用一个支付场景讲清三件事:
- 幂等性到底是什么;
- 为什么分布式系统里重复请求是常态;
- 如何用可落地方案做接口防重。
一、先给结论:幂等性的核心定义
一句话定义:
同一个操作执行 1 次和执行 N 次,对系统最终状态的影响一致。
例如:
GET /order/123反复查订单,状态不变,天然幂等;PUT /user/1/profile用同一份数据重复更新,最终结果一致,通常可做成幂等;POST /pay如果每次都新建支付流水,重复调用就会重复扣款,默认不幂等。
所以重点不是"请求有没有重复",而是"重复后状态是否可控"。
二、重复请求为什么一定会出现
很多系统不是"偶尔"重复,而是"必然"重复。常见来源有:
- 用户层重试:按钮连点、刷新页面、网络抖动后重复提交;
- 客户端自动重试:超时后 SDK/网关自动补发;
- 消息系统至少一次投递:消费者可能收到同一条消息多次;
- 服务链路重放:代理层、任务补偿、人工重放脚本。
这意味着:你不能把"不要重复发请求"当成唯一策略,后端必须有幂等兜底。
三、最常用方案:幂等键(Idempotency-Key)
3.1 思路
客户端在请求头带一个唯一键,例如:
http
POST /api/pay
Idempotency-Key: pay_20260428_9f3c2f
服务端收到后,先看这个键是否处理过:
- 没处理过:执行业务,写入结果缓存/记录;
- 处理过:直接返回第一次处理结果,不再二次扣款。
3.2 最小伪代码(Redis 版)
js
// 伪代码:表达流程,不是生产模板
async function createPayment(req) {
const key = req.headers["idempotency-key"];
if (!key) throw new Error("Missing Idempotency-Key");
const lockKey = `idem:lock:${key}`;
const resultKey = `idem:result:${key}`;
// 1) 先查是否已有结果
const cached = await redis.get(resultKey);
if (cached) return JSON.parse(cached);
// 2) 尝试加锁,避免并发重复处理
const locked = await redis.set(lockKey, "1", { NX: true, EX: 30 });
if (!locked) {
// 有并发中的同key请求,可返回"处理中"或短暂重试
throw new Error("Request is processing, retry later");
}
try {
// 3) 执行业务(扣款、落库、发消息)
const result = await payAndPersist(req.body);
// 4) 记录结果(设置合理TTL)
await redis.set(resultKey, JSON.stringify(result), { EX: 24 * 3600 });
return result;
} finally {
await redis.del(lockKey);
}
}
3.3 实践注意点
- 幂等键应和"业务唯一意图"绑定(如订单号+用户+操作类型),不要只用随机字符串;
- TTL 过短会导致"晚到重试"失效,TTL 过长会占资源;
- 返回历史结果时,建议包含"本次为幂等命中"的标记,方便排查。
四、除了幂等键,还要配合数据库唯一约束
单靠缓存还不够。真正防事故,数据库层建议再加一道硬约束,例如:
- 支付流水表对
biz_order_id做唯一索引; - 或对
out_trade_no做唯一键。
这样即使上层防重失效,数据库也能防止重复写入。常见模式是:
- 业务先做幂等键快速拦截;
- DB 唯一键做最终兜底;
- 捕获唯一键冲突后,返回首次结果或明确提示"已处理"。
五、容易踩坑的 4 个点
5.1 幂等键只在单实例内生效
如果你把状态放进进程内存,多实例部署后会直接失效。应使用共享存储(Redis/DB)。
5.2 锁住了请求,没锁住副作用
如果"扣款成功"与"落库成功"不在同一一致性策略中,仍可能出现"扣了钱但没记录"。要配合事务/补偿机制。
5.3 把"查询接口幂等"误当"写接口幂等"
读接口天然更容易幂等,写接口才是风险区,不要混为一谈。
5.4 忽略回调场景
第三方支付/物流回调常会重复通知;回调处理器必须幂等,不然还是会重复改状态。
六、如何判断你们系统做得够不够
可以用这份快速检查清单:
- 写接口是否定义了明确幂等策略(键、唯一约束、返回语义)?
- 并发同键请求是否可控(锁或原子操作)?
- 重试、超时、补偿、回调是否统一走幂等路径?
- 监控里是否有"幂等命中率/重复请求率/唯一键冲突率"?
如果这四项都能回答清楚,重复扣款类事故通常会下降很多。
总结
幂等性不是"优雅设计",而是高并发与分布式系统的生存能力。
你可以把它记成一句工程化原则:请求可以重复到来,但业务结果只能生效一次 。
落地时建议"双保险":幂等键 + 数据库唯一约束,再配合重试与回调链路治理。
你们线上现在最容易重复执行的是支付、下单,还是消息消费?评论区说下你的真实场景,一起交流学习~~~。