高并发下订单状态更新

2022-03-09 10:37:03 +08:00
 frank1256

场景

订单存在 flag 字段,0 未支付,1 支付中,2 支付完成。 发起支付场景中,会先查询订单状态是否为 0 ,然后更新为 1 ,并且调用第三方支付系统获取 h5 的支付地址(耗时操作)。用户在 h5 上完成支付后,第三方支付系统会异步通知到后台服务。进行订单更新动作,并保存流水号。

具体代码

支付发起

支付发起之前,会查库,判断 flag 是否为 0 ,可以的才会继续

异步通知

接收到第三方系统的异步通知后,会查库,判断 flag 是否为 1 ,可以的话才会更新订单。

问题

高并发下,第一个线程查库,查到 flag 是 0 ,在数据库没更新完成的情况下,第二个线程也来查库,查到是 flag 也是 0.同时发起了支付。如何防止这种场景呢?假设在单节点情况下,直接加 synchronized ,可以避免。但是这样的话,是对所有的线程都进行了阻塞,实际情况下,我们只是要对相同订单进行阻塞。不同订单不进行阻塞的。

在异步回调的情况也是一样,也是要先查订单状态 flag 为 1 的话,才会进行下一步动作,如果并发情况下出现了 2 个线程都查到是 flag 为 1 怎么处理?

目前思路

加锁,但是锁了所有的线程,订单 1 多个线程同时发起支付的话,需要加锁阻塞,只能有一个发起成功,但是不能影响订单 2 的发起支付。实际上只是为了锁同一笔订单。

用乐观锁,然后数据库 update 的时候,where flag=某个条件。一定会有一个线程更新失败,更新成功的才会进行后续操作。这样的话,会对数据库有影响吗?

想请问大佬们,这种先查库得到条件,再根据条件做后续动作的场景,在高并发下应该如何处理呢?

8463 次点击
所在节点    Java
78 条回复
encro
2022-03-09 20:20:30 +08:00
1,订单是可以重复支付的,用户第一次用微信发现不够,再用支付宝,那么一个订单就是有两个支付订单了!!!

2,避免订单订单重复支付是前台要提供的,前台要阻塞。

所以:

1, 订单支付状态只有 0 和 1,没必要有支付中。

2, 订单和支付最好分开来,这样每个都是可溯源了

3, 后台只需要在 callback 时根据状态决定是否继续处理即可。

update payment set paid=1 where paid=0 and id=x;

根据结果再

update order set paid=1 where paid=0 and id=x;

根据结果再发送订单支付消息之类
teem
2022-03-09 22:37:55 +08:00
1 、不需要中间状态。
2 、支付动作(拉起收银台)只生成订单,订单的更新只根据回调修改
3 、支付回调全部进全局队列,一个一个操作。
documentzhangx66
2022-03-09 22:49:54 +08:00
年代不一样了,请别再用古老的阻塞方式,包括且不限于:悲观锁、行锁、表锁甚至库锁、分布式锁。

现代的处理方式是:

1.水平分库。电商业务,按用户进行分库,不同库跑在不同节点上,从源头上就减少了并发量。每个节点只处理一小部分用户的数据。

2.使用查询 + 增加数据版本的方式,来代替更新与删除。现代数据库应该尽量少地出现更新数据的操作,第一是为了保留历史数据,第二是现代设备是锁的同步代价远大于存储代价。

3.拥抱异常,把一单多付的场景视为正常情况考虑,做好出现这种情况后进行退款的流程与自动化即可。
Leviathann
2022-03-10 00:54:44 +08:00
@documentzhangx66 第 2 条,这样的话就把一个数据的所有历史版本都记录下来吗?
winglight2016
2022-03-10 07:41:00 +08:00
数据库别锁了,没有价值,#31 说的对,用户真的傻夫夫支付两次,只需要退一笔就可以了。业务流程不需要阻止什么操作,只要让流程走下去,而且在需要的时候可以回退就够了。

另外,楼上也说过,别搞第三种状态,只需要是否已支付两种状态就够了。
Jekins
2022-03-10 09:13:54 +08:00
@documentzhangx66 第二条所有历史版本都保存下来.请问后期如何统计订单数据 ?
undefine2020
2022-03-10 09:16:36 +08:00
没搞懂,为什么一个用户可以操作出高并发的情景出来
cheng6563
2022-03-10 10:28:50 +08:00
@Jekins 多半是把数据再导到仓库去进行数据分析。这就是现代的不计成本体量的处理方式
documentzhangx66
2022-03-10 12:18:35 +08:00
@Leviathann 是的
documentzhangx66
2022-03-10 12:20:02 +08:00
@Jekins

分组或去重。
zw1one
2022-03-10 12:32:55 +08:00
### 不同用户的不同订单,是不会出现你说的问题的(你说的全局 synchronized 又是另外一个问题了)。这里我假设你要处理的问题是: 相同用户对一笔相同订单重复提交(多个用户来提交一笔订单也成立,扫码点餐)

- 场景 1: 用户用浏览器 A 登录,发起一笔支付,在支付结果返回前,再用浏览器 A 发起支付
通常用前端校验,但前端校验可绕过。后台需要用"订单号 ID"加 redis 分布式锁校验,若不能获取到锁,则代表该订单有处理中且未返回的支付请求,拒绝该次请求。

- 场景 2: 用户用浏览器 A 登录,发起一笔支付,在支付结果返回前,再用浏览器 B 发起支付
该情况前端无法校验。后台同样是 redis 锁处理。

- 场景 3: 用户用浏览器 A 登录,发起一笔支付,在支付结果返回后,再用浏览器 B 发起支付
该情况前端无法校验。后台通常在数据库表加上 data_version 字段处理,这里你用订单 flag 字段判断也可解决。

结论:
我没理解错的话,你这个问题是接口幂等问题。需要保证一个接口被多次调用(相同或不同客户端)得到的结果相同。
- 前端校验: 拦截部分客户端重复提交问题,但不能完全解决。
- redis 锁校验: 解决请求未处理完成,又出现新请求的情况。直接拒绝新请求。
- data_verison 校验(或者 flag 字段): 解决请求处理完成后,再次发起请求的情况。

### 至于异步回调,也是幂等问题。
如果你的支付申请处理好了,是不会出现两次回调的,除非第三方出问题了。
如果支付申请没处理好,出现两次回调,且订单 flag 都查到为 1 ,它们的操作都是修改订单结果为 2 ,代码运行两次是没有问题的。mysql 处理逻辑:先修改订单 flag 的事务 A 会给该条数据加写锁,事务 B 修改订单 flag 会等待获取锁。
出现这种情况把异步回调的日志记录好就行。

### 其他
- synchronized 无法处理一个应用部署多个副本的集群情况。可以按对象加锁。
- flag 字段,0 未支付,1 支付中,2 支付完成。
建议保留状态"支付中",该状态可以表示等待第三方回调,当请求发出去,第三方出现问题(超时、宕机)没有回调的时候,便于排查问题。
- 做好上面这些。再来考虑异常退款给用户的人工操作。因为即使代码上处理了,还会有服务器宕机、第三方平台问题等情况出现。生产问题总归是少不了的 :)
frank1256
2022-03-10 13:34:34 +08:00
@zw1one 感谢解答
sakasaka
2022-03-10 14:33:04 +08:00
分布式锁
seasonsolt
2022-03-10 14:40:59 +08:00
@documentzhangx66 本帖唯一高质量回答,和我们现在的处理方式比较接近,分布式 & lock free
1:首先订单系统根据用户请求做了 event 分发降低了并发量,数据库设计面向 event sourcing ,只增量记录 event log 2:重复支付发生时,订单实收 > 应收,追述到支付事件的 event log ,拿到支付凭证发起退款就可以了。 3:无论是前端锁还是后端锁,用户支付体验都会受影响,而且影响系统吞吐量
Chinsung
2022-03-10 17:17:41 +08:00
@seasonsolt #74 和你方案一样的就是高质量回答是吧,我比较好奇,你们这种做法
1. 如果用户重复支付了,会在用户端看到自己支付了多笔并且有几笔正在退款的信息吗?
2. 如果退款某笔失败了,你们是无限轮询还是手工处理,如果退款出现延时,难道用户就没有意见不会投诉吗?
3. 对账的话,如果跨清算时间退款了,对账复杂度也会比较高吧
一个用户 id+订单 id 加分布式锁的问题,对吞吐的影响真的有那么大?
seasonsolt
2022-03-10 18:19:30 +08:00
@Chinsung 你的疑问全部理解,其实这个帖子里的我所谓的“不那么高质量”方案,我们都经历过,或者说这个行业都经历过,敢妄评“优劣”自然是建立无数的实践和采坑的基础之上的。 实践远比我的回答有意义,你可以去测试一些非 sdk 支付场景,支付成功后立马断网(飞行模式),或者杀掉支付 app ,切进程到商户收营台 再次支付。有些平台可以正常支付,但是很快自动退款,还有戏平台,会弹出报错 “您当前订单正在支付中,balabala.......” 。最后,做个统计,是不是实力更强的平台选择了 自动退款方案,比如 麦当劳、KFC...然后锁定单是不是相对弱一些(除了滴滴打车)
seasonsolt
2022-03-10 18:23:46 +08:00
@Chinsung 第三个问题确实是存在的,如果恰好退款时间恰好跨清算批次了,处理起来自然是恶心的,但是概率应该很小吧。 然后是纯技术的,lock free 更多算是一种技术洁癖吧,不一定能有多大性能损失,毕竟不是每一家都能做到 jd 、tb 的量级的。
9c04C5dO01Sw5DNL
2022-03-11 00:06:51 +08:00
@Chinsung 那位搞的是 cqrs ,cqrs 有 cqrs 的弊端。谈什么高质量不高质量。

另外,做幂等接口不是必须用锁的。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/839046

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX