接口幂等:从一次重复扣款说起
对账时发现一笔订单扣了两次款。查日志:支付回调超时,支付平台重发了一次,而我们的回调接口没有防重。这不是 bug,是假设错了——默认每条请求只会来一次,而网络世界里重试才是常态。
幂等的思路很统一:给请求一个全局唯一的身份,然后保证"同一个身份只生效一次"。落到实现,常用三招:
- 数据库唯一索引:把请求 ID 插入专门的防重表,插入成功才处理业务。最朴素,也最可靠——并发场景下数据库帮你把互斥做了
- Redis 原子占位:
SET key NX EX 300,占位成功才往下走。性能好,但要考虑 Redis 挂了怎么办(降级回数据库) - 状态机:订单状态只能单向流转,
UPDATE orders SET status=2 WHERE id=? AND status=1,影响行数为零就说明处理过了

最容易踩的坑:先 SELECT 再 INSERT。两个并发请求都查到"不存在",然后都插入成功。防重必须靠原子操作(唯一索引、SET NX、条件更新),不能靠"先查一下"。
另一个经验是防重范围要对齐业务。支付回调防重用支付流水号,前端表单防重用页面一次性令牌——拿别人的单号防自己的重试,等于没防。做设计评审时我只问一个问题:"这条请求的唯一身份是什么?两个相同的请求会带一样的身份吗?"答案清楚,方案就清楚。
那次重复扣款之后,所有回调入口统一加了防重表,顺手把"重试也安全"写进了接口设计模板的第一条。