跳到主要内容
弗美科技
服务案例

军利易购积分模块,重复提交多出 3 笔

军利易购的会员积分,上线第二周发现同一个订单加了 3 次分。不是并发,是用户在结果页连点了刷新,而加分接口没做幂等。后来给每笔积分变动加了业务唯一键,也就是订单号加类型,Redis 里存一个 10 分钟的请求令牌,重复的直接返回第一次的结果。

2019年11月22日积分幂等电商

积分模块上线第二周,客服转来一条:有个会员买了 1 单,积分流水里躺着 3 条一模一样的加分记录。

先排除并发

第一反应是并发扣减那类问题,查了日志,三条请求时间差了 4 秒和 7 秒,不是同时来的。再看 user-agent 和请求路径,是同一个浏览器在订单结果页连着刷新了两次。

原因是加分接口写成了「收到请求就 insert 一条流水」,没有任何去重。用户点慢一点、网络转个圈,多点两下,分就多加了。

幂等键怎么定

我们没用请求里带的随机数,那种防不住刷新。用的是业务本身能确定的东西:order_id 加 change_type。一个订单的「下单加分」只可能有一条,「退货扣分」是另一个 type,各自独立。

数据库里给积分流水表加了唯一索引 (order_id, change_type),重复插入直接报冲突,后端捕获后不报错,把第一次那条记录查出来原样返回。前端拿到的还是成功,用户看不出区别。

这一步最关键的是「重复请求返回原结果,而不是返回错误」。返回错误,用户会以为没加成,再点一次。

前端那层

前端提交后按钮置灰,结果页跳走时把地址换成另一个,避免刷新重新 POST。这层只是让人少点两下,不当真。真正兜底的还是数据库那唯一索引。

另外在 Redis 存了一个请求令牌,10 分钟过期,同一个令牌重复进来直接短路。这层是为了拦那些连唯一索引都走不到的场景,比如发短信、调外部接口,成本比一次数据库写入低。

修数据

已经多出来的那 2 条不能直接删,得留痕。我们加了一张 adjustment 记录,按订单号把 3 条合成 1 条,多出的写一条负数冲掉,备注写清是重复提交修正。一共核出 11 个订单受影响,跑了半小时脚本,导出一张明细给客服留底。

后来把「所有会改余额的接口」列了个单子,一共 6 个,逐个补了幂等键。有两个当时没出过问题,也一起补了。

那次之后我们有个习惯:凡是带「加」「减」「扣」「发」字眼的接口,评审时先问一句,重复调用会怎样。

商务咨询 · 免费初步沟通

需要针对你的场景给个建议?

把业务背景和当前系统情况告诉我们,我们会给出可执行的思路。

在线留言

周一至周五 9:00–18:00 · 工作日 9:00–18:00 响应,留言会在一个工作日内回复。

商务微信
商务微信二维码

扫一扫,加商务微信咨询服务与方案报价

或填写需求表单