得物面试:10wqps高并发,如何防止重复下单?
文章很长,且持续更新,建议收藏起来,慢慢读!疯狂创客圈总目录 博客园版 为您奉上珍贵的学习资源 :
免费赠送 :《尼恩Java面试宝典》 持续更新+ 史上最全 + 面试必备 2000页+ 面试必备 + 大厂必备 +涨薪必备
免费赠送 :《尼恩技术圣经+高并发系列PDF》 ,帮你 实现技术自由,完成职业升级, 薪酬猛涨!加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷1)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷2)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷3)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 资源宝库: Java 必备 百度网盘资源大合集 价值>10000元 加尼恩领取
得物面试:10wqps高并发,如何防止重复下单?
尼恩特别说明: 尼恩的文章,都会在 《技术自由圈》 公号 发布, 并且维护最新版本。 如果发现图片 不可见, 请去 《技术自由圈》 公号 查找
此文的公众号版本 炸裂:MySQL死锁是什么,如何解决?
尼恩说在前面
在40岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如得物、阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试资格,遇到很多很重要的面试题:
10wqps高并发,如何防止重复提交/支付订单?
10wqps高并发,如何防止重复下单?
10wqps高并发,如何防止重复支付?
10wqps高并发,如何解决重复操作问题?
最近有小伙伴在面试得物,又遇到了这个的面试题。小伙伴支支吾吾的说了几句,面试官不满意,面试挂了。
所以,尼恩给大家做一下系统化、体系化的梳理,使得大家内力猛增,可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。
当然,这道面试题,以及参考答案,也会收入咱们的 《尼恩Java面试宝典PDF》V171版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。
最新《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请关注本公众号【技术自由圈】获取,回复:领电子书
基础知识:电商订单支付核心流程
首先,来看看 订单支付的业务流程和交互流程。
图解:订单支付的业务流程和交互流程
结合下图来看看 订单支付的业务流程和交互流程。
订单支付流程, 分为 大致 的 6个步骤 :
1.下单/结算:
下单作为支付的入口,但并非起点,
支付相关的金额等信息全部来至结算,此时订单处于 未支付 状态。
2.申请支付:
用户开始申请支付,客户端调用支付服务,
此时在支付系统内产生一笔订单支付流水,这笔支付流水处于 未支付 状态。
3.发起支付:
支付服务调用 第三方支付平台,
通常, 第三方支付平台 是 钱包类的支付方式,
在发起支付这一步骤,支付平台会响应一些支付的链接,客户端会对链接进行相应的处理。
4.钱包支付:
用户进行支付,
用户 APP端直接拉去钱包进行支付。
5.支付回调:
用户完成支付之后,三方支付平台会回调 商户的支付服务 接口,通知支付结果。
6.更新订单状态:
支付服务 确认订单支付完成后,会向 订单服务同步 支付的结果。
订单服务变更服务的状态:未支付
变更为 待发货
。
客户端通过轮询、长轮询,或者服务端主动推送的方式,在界面上变更订单状态。
图解:支付状态的变化
如下图,从支付流水角度来分析一下支付状态的变化:
1.从未支付,到有支付结果的终态,中间还有一个中间状态:支付中
2.户通过打开钱包--》完成支付--》支付回调,这段时间的支付流水就处于:支付中
重复下单的定义、危害、应对策略
什么是重复下单
现在问题来了, 什么是重复下单?
用户在下单页面进行下单时,由于用户点击下单按钮 多次 、或者 重试策略 导致在订单服务中接收到了 两次同样 的下单请求。
重复下单带来的危害
重复下单场景,第N次的下单会对数据进行打乱,导致系统整体数据异常
- 库存数据异常
- 金额数据异常
- 优惠券数据异常
- 等等
重复下单场景,第N次的下单需要等第一次下单操作完成
重复下单带来的危害, 总结起来,有以下几点:
1.系统资源占用与性能下降
-
重复下单会占用系统资源,包括服务器、数据库等,特别是在下单高峰期,可能导致系统性能下降,响应速度变慢。
-
重复请求可能引发系统拥堵,影响其他正常用户的购物体验。
2.订单处理复杂性增加
- 商家在处理订单时,需要花费额外的时间和精力去识别、合并或取消重复订单,增加了订单处理的复杂性。
- 重复订单可能导致库存数量出现错误,进而影响后续订单的履行。
3.财务结算与对账难度增大
- 重复下单可能导致财务结算时出现混乱,需要花费更多时间和精力去核对和调整账目。
- 对账过程中需要区分哪些是重复订单,哪些是有效订单,增加了对账的难度。
4.用户体验受损
- 消费者在遇到重复下单时,可能会感到困惑和不满,影响对电商平台的信任度和忠诚度。
- 重复下单可能导致消费者错过优惠活动或促销时机,影响其购物体验。
5.数据异常与决策误导
- 重复下单的数据会干扰销售数据的准确性,可能导致商家在决策时受到误导。
- 错误的销售数据可能影响商家的库存规划、生产计划等关键决策。
6.售后服务与退换货问题
- 如果消费者对重复下单的商品申请了退换货,会增加售后服务的处理难度和成本。
- 重复订单可能导致退换货政策执行混乱,影响消费者的售后体验。
7.安全风险与欺诈行为
- 重复下单有时可能是恶意行为,如刷单、欺诈等,给电商平台带来安全风险。
- 需要重点加强对重复下单的监控和识别,以防范潜在的安全风险。
重复下单问题,主要解决办法就是做好幂等,因为在分布式系统中,我们是没有办法保证用户一定不会快速点击两次下单。
Order 服务调用 Pay 服务,刚好网络超时,然后 Order 服务开始重试机制,于是 Pay 服务对同一支付请求,就接收到了两次,而且因为轮询负载均衡算法,请求落在了不同业务服务节点,所以一个分布式系统服务,须保证幂等性。
什么场景下回发生重复下单?
场景1:客户端bug
用户短时间内多次点击下单按钮,或浏览器刷新按钮导致。
比如下单的按键在点按之后,在没有收到服务器请求之前,按键的状态没有设为已禁用状态,还可以继续点击。又或者,在触摸屏下,用户手指的点按可能被手机操作系统识别为多次点击。
场景2:超时重试
Nginx或Spring Cloud Gateway 网关层、RPC通信重试或业务层重试,进行超时重试导致的。
用户的设备与服务器之间,可能是不稳定的网路。这样一个下单请求过去,服务器不一定及时返回结果。
超时最大的问题:从用户的角度,他无法确定下单的请求是否达到服务器,还是已经到了服务器但是返回结果时数据丢失了。所以用户无法区分到底这个订单是否下单成功。
场景3:用户APP强退/闪退之后重新下单
心急的用户可能会重启流程/重启App/重启手机。在这种强制的手段下,任何技术手段都会失效。
场景4:黑客或恶意用户
黑客或恶意用户使用postman等网络工具,重复恶意提交订单。
重复下单问题与幂等性问题
重复下单问题,本质上,就是下单操作的幂等性问题
说到底,“下单防重”的问题是属于“接口幂等性”的问题范畴。
什么是幂等性问题?
所谓幂等性,就是一次操作和多次操作同一个资源,所产生的影响均与一次操作的影响相同。
"幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。
幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。
幂等性,用数学语言表达就是:
f(x)=f(f(x))
维基百科的幂等性定义如下:
幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。
幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。
这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。
例如,“setTrue()”函数就是一个幂等函数,无论多次执行,其结果都是一样的,更复杂的操作幂等保证是利用唯一交易号(流水号)实现.
通俗点说:
一个接口如果幂等,不管被调多少次,只要参数不变,结果也不变。
幂等性是对于写操作来说的,一个写操作,一般都需要保证:
-
幂等性
-
可用性
-
ACID事务属性。
上述内容选自尼恩这篇硬核文章:
如何解决接口幂等问题
接口接口幂等问题,只需记住一句口令:一锁、二判、三更新。只需严格按照这个过程,那么就可以解决接口幂等问题,总结如下:
1.一锁:先加锁,可以加分布式锁、悲观锁都可以,但是一定是一个互斥锁。
2.二判:进行幂等性判断,可以基于状态机、业务流水表、数据库唯一索引等,进行重复操作的判断。
3.三更新:对数据进行更新,将数据进行持久化。
关于幂等性方案,请参见尼恩这篇硬核文章:
如何解决重复下单问题?
方案一:提交订单按钮置灰
防止用户提交,最常规的做法,就是客户端点击下单之后,在收到服务端响应之前,按钮置灰。
前端页面直接防止用户重复提交表单,但网络错误会导致重传,很多RPC框架、网关都有自动重试机制,所以重复请求在前端侧无法完全避免。
当然,这种方案也不是真的没有价值。
这种方案可以在高并发场景下,从浏览器端去拦住一部分请求,减少后端服务器的处理压力,达到过滤流量的效果。
方案一优点:简单。基本可以防止重复点击提交按钮造成的重复提交问题。
方案一缺点:前进后退操作,或者F5刷新页面等问题并不能得到解决。
方案二:请求唯一ID+数据库唯一索引约束
首先来向大家介绍一种最简单的、成本最低的解决方案。
防重是第一步,需要识别是否重复请求,
所以,需要客户端在请求下单接口的时候,需要生成一个唯一的请求号:requestId
,服务端拿这个请求号,判断是否重复请求。
核心流程图:
实现的逻辑,流程如下:
-
当用户进入订单提交界面的时候,调用后端获取请求唯一ID,并将唯一ID值埋点在页面里面。
-
当用户点击提交按钮时,后端检查这个唯一ID是否用过,如果没有用过,继续后续逻辑;如果用过,就提示重复提交。
-
最关键的一步操作,就是把这个唯一ID 存入业务表中,同时设置这个字段为唯一索引类型,从数据库层面做防止重复提交。
对于下单流量不算高的系统,可以采用这种 请求唯一ID + 数据表增加唯一索引约束`的方式,来防止接口重复提交!
但是这个并发量太低,10wqps高并发, 这个根本没法满足。
方案三:reids分布式锁+请求唯一ID
在上一个方案中,我们详细的介绍了对于下单流量不算高的系统,可以通过 请求唯一ID+数据表增加唯一索引约束`这种方案来实现防止接口重复提交!
随着业务的快速增长,每一秒的下单请求次数,可能从几十上升到几百甚至几万。
面对这种下单流量越来越高的场景,此时数据库的访问压力会急剧上升,数据库会成为整个下单流程的瓶颈。
对于这样的场景,我们可以选择引入缓存中间件来缓解数据库高并发场景下的压力,
下面,我们以引入redis
缓存中间件,向大家介绍具体的解决方案。
流程如下:
-
当用户进入订单提交界面的时候,调用后端获取请求唯一 ID,同时后端将请求唯一ID存储到
redis
中再返回给前端,前端将唯一 ID 值埋点在页面里面。 -
当用户点击提交按钮时,后端检查这个请求唯一 ID 是否存在,如果不存在,提示错误信息;如果存在,继续后续检查流程。
-
使用
redis
的分布式锁服务,对请求 ID 在限定的时间内进行加锁,如果加锁成功,继续后续流程;如果加锁失败,提示说明:服务正在处理,请勿重复提交。 -
最后一步,如果加锁成功后,需要将锁手动释放掉,以免再次请求时,提示同样的信息;同时如果任务执行成功,需要将
redis
中的请求唯一 ID 清理掉。
至于数据库是否需要增加字段唯一索引,理论上可以不用加,如果加了更保险。
这个通过扩展,可以满足 10wqps高并发要求。
具体的扩展方案, 即将在 《尼恩Java面试宝典》 配套视频 发布。
方案四:reids分布式锁+token
在上一个方案中,每次提交订单的时候,都需要调用服务端获取请求唯一ID:requestId,然后才能提交,这里面存在以下问题:
下单链路中,多了的一次请求, 这一次请求专门用于请求 request id。这次请求是否可以减少呢?
当然是可以的,比如, 可以用户的请求的特征数据,根据特定规则生成token,来替代 那个专用的 request id。
而不用专门去来减少一次客户端与服务端之间的交互次数,提高下单流程效率。
特定规则生成token, 比如说,可以组合一些核心参数,去生成token, 核心参数包括:
应用名+接口名+方法名+请求参数签名(请求header、body参数,取SHA1值)
组合一些核心参数,去生成token ,大致 流程如下:
-
用户点击提交按钮,服务端接受到请求后,通过规则计算出本次请求唯一ID值
-
使用
redis
的分布式锁服务,对请求 ID 在限定的时间内尝试进行加锁,如果加锁成功,继续后续流程;如果加锁失败,说明服务正在处理,请勿重复提交。 -
最后一步,如果加锁成功后,需要将锁手动释放掉,以免再次请求时,提示同样的信息。
方案四和方式三的最大不同,在于 唯一请求 ID 的生成 环节,
方案四 放在服务端通过组合来实现 唯一请求 ID 的生成 ,在保证防止接口重复提交的效果同时,也可以显著的降低接口测试复杂度!
方案四的性能,比方案三更高。
方案五:技术+产品+运营支持
如果经过上述方案处理,还是会有用户误操作,直到收到两份商品才发现下重了。
在实际设计中,无论多么好的技术,也不可能100%的拦截所有的可能性,必须依靠技术+产品设计+运营支持
的综合手段才能解决这类问题。
此时就得依靠运营/客服的支持了。
所以即便京东这一类电商等也是配合运营手段进行处理。
实操:reids分布式锁+token 解决重复下单的问题
只讲理论,是耍流氓
40岁老架构师一直强调, 实操,实操,实操才是王道
比如咱们社群的 k8s 实操:
比如咱们社群的 AT+TCC模式混合事务实操 ):
此实操即将配合 《尼恩Java面试宝典视频》发布
接下来,咱们开始 reids分布式锁+token 解决重复下单的问题的实操
此实操即将配合 《尼恩Java面试宝典视频》发布
实操step1:使用AOP进行 BizToken 的无入侵生成
定义一个注解 BizToken
在业务层或者 控制层,进行BizToken 的使用
实操step2:编写服务验证逻辑,通过 aop 代理方式实现
此 aop 切面的 具体的实操演示,请参见 《尼恩Java面试宝典》 视频
实操step3:使用redission分布式锁保证幂等
在BizToken校验逻辑用到了redis
分布式锁保证幂等,
redission分布式锁 具体实现逻辑如下:
通过封装 redission的分布式锁来实现 锁的功能:
具体的实现,委托到 redission的分布式锁来实现
具体的实操演示,请参见 《尼恩Java面试宝典》 视频
10wqps高并发,防止重复下单总结
防止重复下单,本质上就是先做重复判断,然后服务端做好幂等性控制,结合实际业务场景选择相应的方案。
实现幂等性需要先理解自身业务需求,根据业务逻辑来实现这样才合理,处理好其中的每一个结点细节,完善整体的业务流程设计,才能更好的保证系统正常运行。
说在最后:有问题找老架构取经
10wqps高并发,如何防止重复下单?
如果大家能对答如流,如数家珍,基本上 面试官会被你 震惊到、吸引到。
最终,让面试官爱到 “不能自已、口水直流”。offer, 也就来了。
在面试之前,建议大家系统化的刷一波 5000页《尼恩Java面试宝典PDF》,里边有大量的大厂真题、面试难题、架构难题。很多小伙伴刷完后, 吊打面试官, 大厂横着走。
在刷题过程中,如果有啥问题,大家可以来 找 40岁老架构师尼恩交流。
另外,如果没有面试机会,可以找尼恩来改简历、做帮扶。
遇到职业难题,找老架构取经, 可以省去太多的折腾,省去太多的弯路。
尼恩指导了大量的小伙伴上岸,前段时间,尼恩指导一个40岁+被裁小伙伴,拿到了一个年薪100W的offer。
狠狠卷,实现 “offer自由” 很容易的, 前段时间一个武汉的跟着尼恩卷了2年的小伙伴, 在极度严寒/痛苦被裁的环境下, offer拿到手软, 实现真正的 “offer自由” 。
技术自由的实现路径:
实现你的 架构自由:
《阿里二面:千万级、亿级数据,如何性能优化? 教科书级 答案来了》
《峰值21WQps、亿级DAU,小游戏《羊了个羊》是怎么架构的?》
… 更多架构文章,正在添加中
实现你的 响应式 自由:
这是老版本 《Flux、Mono、Reactor 实战(史上最全)》
实现你的 spring cloud 自由:
《Spring cloud Alibaba 学习圣经》 PDF
《分库分表 Sharding-JDBC 底层原理、核心实战(史上最全)》
《一文搞定:SpringBoot、SLF4j、Log4j、Logback、Netty之间混乱关系(史上最全)》
实现你的 linux 自由:
实现你的 网络 自由:
《网络三张表:ARP表, MAC表, 路由表,实现你的网络自由!!》
实现你的 分布式锁 自由:
实现你的 王者组件 自由:
《队列之王: Disruptor 原理、架构、源码 一文穿透》
《缓存之王:Caffeine 源码、架构、原理(史上最全,10W字 超级长文)》
《Java Agent 探针、字节码增强 ByteBuddy(史上最全)》