尼恩说在前面

在40岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如得物、阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试资格,遇到很多很重要的面试题:

10wqps高并发,如何防止重复提交/支付订单?

10wqps高并发,如何防止重复下单?

10wqps高并发,如何防止重复支付?

10wqps高并发,如何解决重复操作问题?

最近有小伙伴在面试得物,又遇到了这个的面试题。小伙伴支支吾吾的说了几句,面试官不满意,面试挂了。

所以,尼恩给大家做一下系统化、体系化的梳理,使得大家内力猛增,可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。

当然,这道面试题,以及参考答案,也会收入咱们的 《尼恩Java面试宝典PDF》V171版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。

最新《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请关注本公众号【技术自由圈】获取,回复:领电子书

本文目录

- 尼恩说在前面

- 基础知识:电商订单支付核心流程

 - 图解:订单支付的业务流程和交互流程

 - 图解:支付状态的变化

- 重复下单的定义、危害、应对策略

 - 什么是重复下单

 - 重复下单带来的危害

    -1.系统资源占用与性能下降
    -2.订单处理复杂性增加
    -3.财务结算与对账难度增大
    -4.用户体验受损
    -5.数据异常与决策误导
    -6.售后服务与退换货问题
    -7.安全风险与欺诈行为

 - 什么场景下回发生重复下单?

- 重复下单问题与幂等性问题

 - 什么是幂等性问题?

 - 如何解决接口幂等问题

 - 关于幂等性方案,请参见尼恩这篇硬核文章:

- 如何解决重复下单问题?

 - 方案一:提交订单按钮置灰

 - 方案二:请求唯一ID+数据库唯一索引约束

 - 方案三:reids分布式锁+请求唯一ID

 - 方案四:reids分布式锁+token

 - 方案五:技术+产品+运营支持

- 实操:reids分布式锁+token 解决重复下单的问题

 - 实操step1:使用AOP进行 BizToken 的无入侵生成

 - 实操step2:编写服务验证逻辑,通过 aop 代理方式实现

 - 实操step3:使用redission分布式锁保证幂等

- 10wqps高并发,防止重复下单总结

- 说在最后:有问题找老架构取经

基础知识:电商订单支付核心流程

首先,来看看 订单支付的业务流程和交互流程。

图解:订单支付的业务流程和交互流程

结合下图来看看 订单支付的业务流程和交互流程。

图片

订单支付流程, 分为 大致 的 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)是一个数学与计算机学概念,常见于抽象代数中。

幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。

幂等性,用数学语言表达就是:

<section><span>f(x)=f(f(x))<br></span></section>

维基百科的幂等性定义如下:

幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。

幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。

这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。

例如,“setTrue()”函数就是一个幂等函数,无论多次执行,其结果都是一样的,更复杂的操作幂等保证是利用唯一交易号(流水号)实现.

通俗点说:

一个接口如果幂等,不管被调多少次,只要参数不变,结果也不变。

幂等性是对于写操作来说的,一个写操作,一般都需要保证:

  • 幂等性

  • 可用性

  • ACID事务属性。

上述内容选自尼恩这篇硬核文章:

最系统的幂等性方案:一锁二判三更新

如何解决接口幂等问题

接口接口幂等问题,只需记住一句口令:一锁、二判、三更新。只需严格按照这个过程,那么就可以解决接口幂等问题,总结如下:

1.一锁:先加锁,可以加分布式锁、悲观锁都可以,但是一定是一个互斥锁。

2.二判:进行幂等性判断,可以基于状态机、业务流水表、数据库唯一索引等,进行重复操作的判断。

3.三更新:对数据进行更新,将数据进行持久化。

关于幂等性方案,请参见尼恩这篇硬核文章:

最系统的幂等性方案:一锁二判三更新 

如何解决重复下单问题?

方案一:提交订单按钮置灰

防止用户提交,最常规的做法,就是客户端点击下单之后,在收到服务端响应之前,按钮置灰。

前端页面直接防止用户重复提交表单,但网络错误会导致重传,很多RPC框架、网关都有自动重试机制,所以重复请求在前端侧无法完全避免。

当然,这种方案也不是真的没有价值。

这种方案可以在高并发场景下,从浏览器端去拦住一部分请求,减少后端服务器的处理压力,达到过滤流量的效果。

**方案一优点:**简单。基本可以防止重复点击提交按钮造成的重复提交问题。

**方案一缺点:**前进后退操作,或者F5刷新页面等问题并不能得到解决。 

方案二:请求唯一ID+数据库唯一索引约束

首先来向大家介绍一种最简单的、成本最低的解决方案。

防重是第一步,需要识别是否重复请求,

所以,需要客户端在请求下单接口的时候,需要生成一个唯一的请求号:requestId,服务端拿这个请求号,判断是否重复请求。

核心流程图:

图片

实现的逻辑,流程如下:

  1. 当用户进入订单提交界面的时候,调用后端获取请求唯一ID,并将唯一ID值埋点在页面里面。

  2. 当用户点击提交按钮时,后端检查这个唯一ID是否用过,如果没有用过,继续后续逻辑;如果用过,就提示重复提交。

  3. 最关键的一步操作,就是把这个唯一ID 存入业务表中,同时设置这个字段为唯一索引类型,从数据库层面做防止重复提交。

对于下单流量不算高的系统,可以采用这种 请求唯一ID + 数据表增加唯一索引约束`的方式,来防止接口重复提交

但是这个并发量太低,10wqps高并发, 这个根本没法满足。

方案三:reids分布式锁+请求唯一ID

在上一个方案中,我们详细的介绍了对于下单流量不算高的系统,可以通过 请求唯一ID+数据表增加唯一索引约束`这种方案来实现防止接口重复提交

随着业务的快速增长,每一秒的下单请求次数,可能从几十上升到几百甚至几万。

面对这种下单流量越来越高的场景,此时数据库的访问压力会急剧上升,数据库会成为整个下单流程的瓶颈。

对于这样的场景,我们可以选择引入缓存中间件来缓解数据库高并发场景下的压力,

下面,我们以引入redis缓存中间件,向大家介绍具体的解决方案。

图片

流程如下:

  1. 当用户进入订单提交界面的时候,调用后端获取请求唯一 ID,同时后端将请求唯一ID存储到redis中再返回给前端,前端将唯一 ID 值埋点在页面里面。

  2. 当用户点击提交按钮时,后端检查这个请求唯一 ID 是否存在,如果不存在,提示错误信息;如果存在,继续后续检查流程。

  3. 使用redis的分布式锁服务,对请求 ID 在限定的时间内进行加锁,如果加锁成功,继续后续流程;如果加锁失败,提示说明:服务正在处理,请勿重复提交。

  4. 最后一步,如果加锁成功后,需要将锁手动释放掉,以免再次请求时,提示同样的信息;同时如果任务执行成功,需要将redis中的请求唯一 ID 清理掉。

至于数据库是否需要增加字段唯一索引,理论上可以不用加,如果加了更保险。

这个通过扩展,可以满足 10wqps高并发要求。

具体的扩展方案, 即将在 《尼恩Java面试宝典》 配套视频 发布。

方案四:reids分布式锁+token

在上一个方案中,每次提交订单的时候,都需要调用服务端获取请求唯一ID:requestId,然后才能提交,这里面存在以下问题:

下单链路中,多了的一次请求, 这一次请求专门用于请求 request id。这次请求是否可以减少呢?

当然是可以的,比如, 可以用户的请求的特征数据,根据特定规则生成token,来替代 那个专用的 request id。

而不用专门去来减少一次客户端与服务端之间的交互次数,提高下单流程效率。

特定规则生成token, 比如说,可以组合一些核心参数,去生成token, 核心参数包括:
<section><span>应用名+接口名+方法名+请求参数签名(请求header、body参数,取SHA1值)</span></section>

图片

组合一些核心参数,去生成token ,大致 流程如下:

  1. 用户点击提交按钮,服务端接受到请求后,通过规则计算出本次请求唯一ID值

  2. 使用redis的分布式锁服务,对请求 ID 在限定的时间内尝试进行加锁,如果加锁成功,继续后续流程;如果加锁失败,说明服务正在处理,请勿重复提交。

  3. 最后一步,如果加锁成功后,需要将锁手动释放掉,以免再次请求时,提示同样的信息。

方案四和方式三的最大不同,在于 唯一请求 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自由” 。

部分历史案例