+ 我要发布
我发布的 我的标签 发现
浏览器扩展
斑点象@Edge

短时间内连续多次提交,前后台怎么防止订单重复提交

重复提交原因 其实原因⽆外乎两种: 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勉强可以算上吧。 在真正的⽣产环境下,推荐⽅案四:从订单业务的本质⼊⼿。 原因很简单,整体改动范围⽐较⼩,测试的回归范围也⽐较可控,且技术⽅案复杂度最低。 这样做技术选型的话,也⽐较符合简单可依赖原则。
我的笔记