最近一直在做公司的小程序对接停车场的需求。结果遇到了并发问题,出现一笔订单,用户支付两次的情况。现在对整个支付流程进行梳理。下面讨论的A用户和B用户都是对于同一订单处理的场景,对于不同订单,则没有并发问题。
在停车缴费页面,可以手输车牌,因此可以多个用户查询同一个车牌的停车费。此时需要保证系统停车费只有一笔。

在上述查询账单表数据过程中,需要以车牌做key值,加上分布式锁。因为可能存在并发,同时有多个用户查询表数据,当表中没数据时,则会插入多条表数据。当A用户对A数据支付,B用户对B数据支付,则会支付两次。
在用户缴费时,当只选择使用账户余额付款时,则可以直接调标准的余额支付接口去付款。

余额支付接口,需要以停车费订单号为key值,加上分布式锁。如果存在多个用户同时进行费用支付,则会因为锁进行排队,无法一起支付。当其中一个用户支付完成后,更新订单状态,则排队的其他用户无法进行支付。
当用户缴费时,有选择券或积分,则可以使用券或积分去抵扣停车费用。

券与积分支付接口需要以停车费订单号为key值,加上分布式锁。如果存在多个用户同时进行费用支付,则会因为锁进行排队,无法一起支付。当其中一个用户支付完成后,更新订单状态,则排队的其他用户无法进行券与积分支付。
当用户缴费时,仅选择微信支付时,则需要用标准的微信预下单接口在微信服务器下单。等前端调用wx.requestpayment组件支付完成,后端收到微信回调结果,则支付完成。

微信支付流程包含多个接口
其中微信预下单接口、后台延迟任务、后端更新订单接口,都需要以订单号为key值,加上分布式锁。
当多个用户同时预下单时,通过分布式锁,使其排队。当其中一个用户下单成功,更新订单状态为微信支付中,则排队的其他用户无法下单。
当多个用户同时调用更新订单状态接口,通过分布式锁使其排队。当其中一个用户执行成功,排队执行的其他用户查询订单状态是已取消或支付失败,则不会执行。
后台延迟任务同时触发,道理同上。
当A用户预下单,B用户同时更新订单状态。由于都持有以订单号为key的分布式锁,会形成排队。此时订单状态应该为微信支付中,因为B用户调更新订单状态接口,说明B用户取消微信支付或微信支付失败。
当在微信预下单后才会可以取消支付或微信支付失败。(1)当A用户先抢到锁,预下单失败。B用户后抢到锁,更新订单为已取消或支付失败。(2)当B用户先抢到锁,更新订单为已取消或支付失败。A用户后抢到锁,预下单失败,提示用户重新查询费用。
A用户预下单,B用户延迟任务使订单过期。由于都持有以订单号为key的分布式锁,会形成排队。因为有延迟任务,所以订单在此之前有进行微信预下单,订单状态为微信支付中或已取消或支付失败或支付成功。上述状态,A用户下单一定失败。B用户触发的延迟任务更新订单为已过期或不处理(状态为支付成功、已取消、支付失败)。
当用户支付时,可以选择优惠券核销和积分抵扣,以及余额付款。包含如下几个接口:
前端判断用户是否有使用券和积分,在用户点击余额付款时,调券与积分支付接口,后台判断如果足够抵扣全部费用,则更新订单状态为支付完成,如果不足够抵扣全部费用,则更新订单状态为支付中,前端调余额付款接口,进行余额付款。

券与积分支付接口需要核销优惠券与积分,更新订单状态为支付中,需要增加事务,当其中一步失败就回滚。同时接口需要加上以订单号为key的分布式锁,当同时两个用户核销券或积分时,会排队处理。当其中一个用户核销券或积分成功后,订单状态为支付中。其他用户则不允许核销券或积分,提示用户订单支付中。
余额付款接口进行余额付款,需要添加事务以及以订单号为key值的分布式锁。当同时两个用户进行余额付款时,则会排队处理。当第一个用户支付成功,则更新订单状态为支付完成,其他用户无法进行余额支付。当第一个用户支付失败,还没调回滚券与积分及订单状态接口时,第二个用户不是同一用户支付,则余额支付失败。
当余额付款失败,调回滚券与积分以及订单状态接口。需要添加事务以及订单号为key值的分布式锁。防重放攻击。当回滚券积分后,更新订单状态为查询完成,则无法重复回滚券与积分。
当A用户进行券与积分支付,B用户进行余额支付。由于两个接口都是以订单号为key作为分布式锁。(1)A用户先抢到锁,则扣券与积分,并更新订单状态为支付中。B用户后持有锁,判断不是同一用户支付,则不会进行余额付款。(2)B用户先抢到锁,则扣除余额,并更新订单状态为支付完成。A用户无法进行券与积分支付。
当A用户进行券与积分支付,B用户调用券与积分回滚接口。由于两个接口都是以订单号为key作为分布式锁。(1)A用户先抢到锁,则判断订单为支付中,无法进行券与积分支付。因为B用户是余额付款失败,才会调用券与积分回滚接口。也就是B用户在此之前成功进行券与积分支付,订单被更新成了支付中。B用户后持有锁,可正常进行券与积分回滚,并更新订单状态为查询完成。(2)B用户先持有锁,可正常回滚券与积分,并更新订单状态为查询完成,A用户后持有锁,可正常进行余额付款,并更新订单状态为支付完成。
当A用户进行余额支付,B用户调用券与积分回滚接口。由于两个接口都是以订单号为key。(1)A用户先抢到锁,则判断支付不是同一用户或费用不等于券金额+积分抵现金额+支付余额,则不能进行余额支付,提示用户重新查询费用。B用户后持有锁,正常回滚券与积分以及订单状态。(2)B用户先抢到锁,可正常回滚券与积分以及订单状态。A用户后持有锁,如果金额正确,可正常进行余额支付。
当三个接口同时发生,则是由上述所说的情况可处理。
当用户支付时,可以选择优惠券核销和积分抵扣,以及微信支付。包含如下几个接口:


券与积分支付接口,微信预下单接口,券与积分回滚接口,延迟任务,微信回调通知接口都以订单号为key,添加分布式锁。
上述接口,当同一种接口发生并发会因为分布式锁进行排队。排队后执行的接口会因为订单状态修改而执行失败。
当两种接口发生并发的场景,【三、券与积分支付接口】和【四、微信支付流程】中已经分析大部分了。下面只阐述券与积分支付接口和微信支付流程中的接口的并发场景。
当A用户进行券与积分支付,B用户进行微信预下单。由于两个接口都是以订单号为key作为分布式锁。(1)A用户先抢到锁,当订单是查询完成状态时,A用户核销券,抵扣积分成功,更新订单状态为支付中。B用户后持有锁,进行微信预下单,不是同一用户支付,无法进行微信下单。当订单是支付中时,A用户先抢到锁,无法核销券与积分,B用户后持有锁,金额正确,则微信下单成功。(2)B用户先抢到锁,当订单是查询完成状态时,下单成功,更新订单状态为微信支付中。A用户后持有锁,因为状态为微信支付中,无法进行券与积分支付。当订单状态是支付中,B用户先抢到锁,如果金额正确,则正常微信预下单,更新订单状态为微信支付中。如果金额不正确,无法微信预下单。A用户后持有锁,由于状态是支付中或微信支付中状态,无法进行券和积分支付。
当A用户进行券与积分支付,B用户进行wx.requestpayment微信支付时。没有分布式锁,但可以根据状态(乐观锁)来锁住。B用户进行微信组件的支付,说明订单状态已经是微信支付中,因此A用户无法核销券和积分。B用户正常进行微信支付。
当A用户进行券和积分支付,B用户微信回调通知。在微信回调通知前,订单状态已经是微信支付中,因此A用户无法进行券和积分支付。
当A用户进行券和积分支付时,B用户执行延迟任务。此时订单为支付中以后的状态,无法进行券和积分支付,券和积分支付只能是查询完成的状态才能正常执行。
当A用户进行接口方式的券和积分回滚,B用户进行微信预下单。因为都是以订单为key作为分布式锁,两个排队执行。由于其中一个接口是券和积分回滚,因此订单状态是支付中或微信支付中。(1)当A用户先抢到锁,券和积分回滚成功,订单状态更新为支付失败,B用户后持有锁,微信预下单失败,提示用户重新查询费用。(2)当B用户抢到锁,如果是支付中,判断不是同一用户支付,则B用户微信预下单失败,A用户正常回滚券和积分。如果是微信支付中,B用户无法预下单,A用户正常回滚券和积分。
当A用户进行接口方式的券和积分回滚,B用户进行wx.requestpayment微信支付时。此场景不存在,因为这两个情况发生在A用户券和积分支付成功,B用户微信预下单成功的情况下。在上面已经分析不会同时成功。除非发生在模拟桩场景,总费用10元,券支付1元,积分抵现1元,通过模拟桩模拟微信预下单8元。此种情况要杜绝的话,需要在券和积分支付后,存用户id到表中,当其他用户进行支付时,判断是否是当前用户操作。
当A用户进行接口方式的券和积分回滚,B用户进行微信回调通知。此场景不存在,因为已经控制了两个用户不会同时进入支付中的状态。
当A用户进行延迟任务的券和积分回滚,B用户进行微信预下单。因为有延迟任务,此时订单为支付中、微信支付中、支付失败、已取消,支付成功的状态。当为支付中时,判断金额不正确,B用户微信预下单失败。当为微信支付中、支付失败、已取消、支付成功场景时,B用户微信预下单失败。
当A用户进行延迟任务的券和积分回滚,B用户进行wx.requestpayment微信支付时。
当A用户进行延迟任务的券和积分回滚,B用户进行微信回调通知接口。一般不会出现,如果出现,则在延迟任务中,判断订单为微信支付中时,调用微信服务器查询订单状态是已过期还是支付成功,做对应处理。