短时间内连续多次提交,前后台怎么防止订单重复提交
重复提交原因
其实原因⽆外乎两种:
1,⼀种是由于⽤户在短时间内多次点击下单按钮,或浏览器刷新按钮导致。
2,⼀种则是由于Nginx或类似于SpringCloud Gateway的⽹关层,进⾏超时重试造成的。
常⻅解决⽅案
⽅案⼀:提交订单按钮置灰
这种解决⽅案在注册登录的场景下⽐较常⻅,当我们点击”发送验证码“按钮的时候,会进⾏⼿机短信验证码发送,且按钮就会有⼀分钟左右的置灰。
有些经验不太丰富的同学,通常会简单粗暴地把这个⽅案直接照搬过来。
但这种⽅案只能解决多次点击下单按钮的问题,对于Nginx或类似于SpringCloud Gateway的超时重试所导致的问题是⽆能为⼒的。
当然,这种⽅案也不是真的没有价值。它可以在⾼并发场景下,从浏览器端去拦住⼀部分请求,减少后端服务器的处理压⼒。
说到底,“下单防重”的问题是属于“接⼝幂等性 ”的问题范畴。
幂等性:f(f(x)) = f(x)
接⼝幂等性是指:以相同的参数,对⼀个接⼝进⾏多次调⽤,所产⽣的结果和⼀次调⽤是完全相同的。
下⾯的情况就是幂等的:
student.setName("张三");
⽽这种情况就是⾮幂等的,因为每次调⽤,年龄都会增加⼀岁。
student.increaseAge(1);
现在我们的思路需要切换到幂等性的解决⽅案来。
同样是幂等性场景,“如何防⽌重复提交订单” ⽐ “如何防⽌订单重复⽀付” 的解决⽅案要难⼀些。因为后者在常规情况下,⼀个订单都是对应⼀笔⽀付单,所以orderID可以作为⼀个幂等性校验、防⽌订单重复⽀付的天然神器。但这个⽅案在“如何防⽌重复提交订单”就不适⽤了,需要其他的解决⽅案。
⽅案⼆:预⽣成全局唯⼀订单号
(1)后端新增⼀个接⼝,⽤于预⽣成⼀个“全局唯⼀订单号”,如:UUID 或 NanoID。
(2)进⼊创建订单⻚⾯时,前端请求该接⼝,获取该订单号。
(3)在提交订单时,请求参数⾥要带上这个预⽣成的“全局唯⼀订单号”,利⽤数据库的唯⼀索引特性,在插⼊订单记录时,如果该“全局唯⼀的订单号”重复,记录会插⼊失败。
该“全局唯⼀订单号”不能代替数据库主键,在未分库分表场景下,主键还是⽤数据库⾃增ID⽐较好。
优点:彻底解决了重复下单的问题;
缺点:⽅案复杂,前后端都有开发⼯作量,还要新增接⼝,新增字段。
⽅案三:前端⽣成全局唯⼀订单号
这种⽅案是在借鉴了“⽅案⼆”的基础上,做了⼀些实现逻辑的简化。
(1)⽤户进⼊下⻚⾯时,前端程序⾃⼰⽣成⼀个“全局唯⼀订单号”。
(2)在提交订单时,请求参数⾥要带上这个预⽣成的“全局唯⼀订单号”,利⽤数据库的唯⼀索引特性,在插⼊订单记录时,如果该“全局唯⼀的订单号”重复,记录会插⼊失败。
优点:彻底解决了重复下单的问题,且技术⽅案做了⼀定简化;
缺点:前后端仍然都有开发⼯作量,且需要新增字段;
⽅案四:从订单业务的本质⼊⼿
先跟⼤家探讨⼀个概念,什么是订单?
其实,订单就是某个⽤户⽤特定的价格购买了某种商品,即:⽤户和商品的连接。
那么,“如何防⽌重复提交订单”,其实就是防⽌在短时间内,⽤户和商品进⾏多次连接。弄明⽩问题本质,接下来着⼿制定技术⽅案。
可以⽤ “⽤户ID + 分隔符 + 商品ID” 作为唯⼀标识,让持有相同标识的请求在短时间内不能重复下单,不就可以了吗?⽽且Redis不正是做这种解决⽅案的利器吗?
Redis命令如下:
SET key value NX EX seconds
把“⽤户ID + 分隔符 + 商品ID”作为Redis key,并把”短时间所对应的秒数“设置为seconds,让它过期⾃动删除。
这样⼀来,整体业务步骤如下:
(1)在提交订单时,我们可以把”⽤户ID + 分隔符 + 商品ID“作为Redis key,并设置过期时间,让它可以到期⾃动删除。
(2)若Redis命令执⾏成功,则可以继续⾛下单的业务逻辑,执⾏不成功,直接返回给前端”下单失败“就可以了。
实现⽅式越来越简单了。
优点:彻底解决了重复下单的问题,且在技术⽅案上,不需要前端参与,不需要添加接⼝,不需要添加字段。
缺点:综合⽐较⽽⾔,暂⽆明显缺点,如果硬要找缺点的话,可能强依赖于Redis勉强可以算上吧。
在真正的⽣产环境下,推荐⽅案四:从订单业务的本质⼊⼿。
原因很简单,整体改动范围⽐较⼩,测试的回归范围也⽐较可控,且技术⽅案复杂度最低。
这样做技术选型的话,也⽐较符合简单可依赖原则。
我的笔记