订单存在 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=某个条件。一定会有一个线程更新失败,更新成功的才会进行后续操作。这样的话,会对数据库有影响吗?
想请问大佬们,这种先查库得到条件,再根据条件做后续动作的场景,在高并发下应该如何处理呢?
1
xujihua 2022-03-09 10:44:01 +08:00 1
mysql 行锁 , 用主键或者唯一索引加锁 SELECT * FROM t1 WHERE c1 = (SELECT c1 FROM t2 FOR UPDATE) FOR UPDATE; 这种方案应该能解决你的问题
|
2
MoYi123 2022-03-09 10:44:27 +08:00
第三方支付不是一般会让你传一个订单号的吗? 至少支付宝是有的.
|
3
Canon1014 2022-03-09 10:44:33 +08:00
synchronized 可以根据业务的 id 上锁,搜索引擎关键字:synchronized id 上锁,当然单节点的前提是不变的
|
4
hcven 2022-03-09 10:44:35 +08:00
key+订单号,用 redis 的 incr 试一下?如果==1 则可以走支付中的逻辑,支付完成后写 redis 标志位再异步写 db 。>1 再去 redis 是不是已经支付完成?
|
5
yibo2018 2022-03-09 10:46:08 +08:00
用数据库的行锁,select ... where orderId = XXX for update 这样就能保证对于一个订单来说,只有一次请求可以获取锁
|
6
micean 2022-03-09 10:48:26 +08:00
flag 为 0 的时候,应该有唯一的订单号在微信 /支付宝那边阻止重复支付
|
7
frank1256 OP |
8
MoYi123 2022-03-09 10:55:32 +08:00
@frank1256 那就`update order set flag=1 where id = 'xxx' and flag = 0` ,
update 返回值是 1 的话发起支付, 和 cas 一个道理. |
10
Chinsung 2022-03-09 11:05:59 +08:00
update 之前的时候加个分布式锁,获取到锁之后进去再 check 下状态是否是 1 ,是 1 直接报已在支付中了,否则 sql 带上 where flag=0 去更新下,根据更新成功行数判断是否需要发第三方支付
虽然你是同订单,其实并不存在高并发只存在并发,但是数据库锁最好少用,一个是得依赖唯一索引,另一个就是真高并发来了,数据库绝对会频繁报死锁 这种场景一般都是设计 2 张表,一张商品订单表,一张支付订单表,商品订单表改状态支付中直接就改了,扔个 mq 给支付订单表去生成支付订单,然后支付订单这里根据商品订单分布式锁做幂等就行了 |
11
timepast 2022-03-09 11:10:07 +08:00
两个层面的问题吧,
1. 如何保证状态一致性,单节点加锁、select ... for update , 分布式锁 等都能解决问题,实质是最小(业务)粒度的一致性,是排他的 2. 上面的问题解决了,订单生命周期,可以设计一个中间状态,即便是高并发,业务也应该有前提限制吧,已经有人支付中了,其他的请求应该失败重试,回调同理 |
12
paradoxs 2022-03-09 11:10:26 +08:00
分布式锁,是现在最优的解决方案。 现实里面,考虑到微服务集群,是不可能用 synchronized 之类的去解决的,没用。
|
14
micean 2022-03-09 11:13:37 +08:00
锁数据库是没办法解决数据库以外的问题的,订单发起多次支付不需要处理,用户永远只会操作其中一次。假如真的出现了支付 2 次的情况,跑任务退款就行了
|
15
ksedz 2022-03-09 11:14:31 +08:00
update set flag = 1 where id = xxx and flag = 0;
然后检查更新的条数,为 1 才能继续。 waitfree 🐶 |
16
wowbaby 2022-03-09 11:15:49 +08:00
一个订单用户很少会重复支付吧,毕竟要付钱的,第三方支付提交的订单号不能重复支付的,至少我目前没遇到过这种问题
|
17
hidemyself 2022-03-09 11:17:56 +08:00
分布式锁,上 redis
|
19
cheng6563 2022-03-09 11:20:39 +08:00
直接落库的业务就直接对数据库行加锁就行了。别搞太复杂。
|
20
k9982874 2022-03-09 11:24:14 +08:00
redis/etcd 加锁
|
21
AS4694lAS4808 2022-03-09 11:24:26 +08:00
@wowbaby 昨天在美团定外卖,支付成功后不点完成,直接用返回按钮,好像会回到支付界面。。没有订阅银行卡或者支付工具通知的人,没准有可能再点一次付钱?
|
22
xiangyuecn 2022-03-09 11:28:38 +08:00 2
这个业务不存在高并发,用户不是机器人,一个订单最多并发两个请求就不错了,前端的问题
|
23
reeco 2022-03-09 11:31:20 +08:00 via iPhone
一锁二判三更新,悲观锁一把梭
|
24
HackerJax 2022-03-09 11:44:51 +08:00 via iPhone
这个不叫并发呀,一个订单只能一个人付款吧,这个应该算作请求去重
|
25
westoy 2022-03-09 11:46:07 +08:00
淘宝、京东、亚马逊、苹果碰到高并发抢购都会出现订单更新不及时、支付更新不及时(尤其像京东、淘宝大部分交易都是走的自己内部金融池子, 还不需要走银联)、掉单, 会通过限制一定周期内取消、更新订单以及客服和财务介入的方式去解决,你就别想通过程序一把梭哈了, 世界是不完美的
|
26
wushigejiajia01 2022-03-09 11:48:08 +08:00 via Android
|
28
timethinker 2022-03-09 12:01:27 +08:00
最好在数据库层面进行并发控制,不要在你的应用层加锁。直接加一个字段使用乐观锁来保证在并发的情况下只有一个事务会成功。并且你需要问自己一个很重要的问题,那就是这个业务真的会有大量的并发请求针对同一个订单进行操作吗?
|
29
yeyypp92 2022-03-09 12:03:13 +08:00
高并发下,前端应该也需要做一些限制,保证用户不会短时间内发起多次支付请求
|
30
shanghai1943 2022-03-09 12:05:56 +08:00
这种场景我一般是乐观锁 update xxx set xxxx=xx where id=xx and flag=0 谁能更新成功那谁就有机会执行往后的逻辑
|
31
Jooooooooo 2022-03-09 12:12:33 +08:00
重复支付退钱就行.
|
32
echooo0 2022-03-09 12:28:42 +08:00
前面几个说的很详细了,数据库行锁就可以搞定,这个应该不属于高并发,算是请求去重;
而且如果是单节点的话,用 synchronized ,也可以针对 id 对象加锁,不是一定要锁住所有线程 |
33
9c04C5dO01Sw5DNL 2022-03-09 12:35:21 +08:00
做成幂等接口就行吧
|
34
oneisall8955 2022-03-09 13:02:33 +08:00 via Android
题外话,我想问下,支付中这个状态是否有必要,多一个状态,就得维护多一个状态的情况。因为网络原因,支付失败,会不会存在永远都是 1 ,再也支付不了的情况?
|
35
frank1256 OP @oneisall8955 会存在,这种情况就是用户支付成功了,但是网络原因,支付系统没有通知过来。这个时候,需要主动发起查询动作,一般支付系统会给 2 个接口,一个是异步通知,一个是主动查询
|
37
frank1256 OP @shanghai1943 我现在的体量就很小,但是难免会遇到一点并发,所以就是想用乐观锁直接 update ,成功的才能往下走。我在想的是,这种操作本质上还是操作数据库了。能否减轻数据库的压力,减少那些会 update 失败的次数
|
38
summerLast 2022-03-09 13:11:10 +08:00
核心就是改成串行 java 相关的话 可以用 RedisLockRegistry 这个
|
39
summerLast 2022-03-09 13:14:45 +08:00
但是要注意 不要内部 方法调用 事务会失效 如 a.method(){this.dotran()}
|
40
summerLast 2022-03-09 13:16:26 +08:00
这是自己封装的例子 可以 事务注解一块修饰 并且能锁住事务
@DistributedLock("wallet:walletId@#{accountId}@#{shopId}") TradingFlow addTradingFlow(String accountId, String shopId, String outerCode, Long amount, TradingFlowTypeEnum type, String remark) |
41
faceRollingKB 2022-03-09 14:33:59 +08:00
不要锁不要队列,只看最终一致性的话,数据库的压力最小
|
42
sanggao 2022-03-09 14:51:55 +08:00
哪来的并发? 这个是幂等
|
43
ZSeptember 2022-03-09 14:53:29 +08:00 1
不是并发情况,数据库乐观锁就可以了。
没有必要,不要引入分布式锁。 |
44
h123123h 2022-03-09 15:14:26 +08:00
你的问题其实有点奇怪,一般来说一个订单号对应一个商品,一个订单号只属于一个人的,不会属于多个人,那就不会存在高并发,只会存在重复提交问题,这个问题乐观锁就能解决。 除非你说的场景是秒杀情况
|
45
godfunc 2022-03-09 15:35:24 +08:00
我做的支付系统是这样处理的 update order set order_no = xxx, status = 2 where trade_no = xx and status = 1 ,update 成功就继续
|
46
tyqing 2022-03-09 15:53:46 +08:00
我们系统是重复支付就退款,一般这种现象极少,除非你们数据库真的性能很差排队入库。
支付回调的地方加 redis 分布式锁保证只有同一个线程在执行支付回调操作,查 redis 中的订单状态和 mysql 的状态判断取最新值,不涉及到数据库锁。 |
47
markgor 2022-03-09 16:09:45 +08:00
这个场景,我只能猜测到是不是代付场景?否则怎么会 第一个线程拉起支付,另一个也拉起支付呢....
如果是代付场景,其实我觉得为了保证付款成功率,没必要限制只能一个人支付, 假设 A 和 B 同时支付了,按通知时间为准,通过事务更新订单状态,另一笔支付通知走事务时发现装维为支付完成的就走退款接口。 |
49
xsqfjys 2022-03-09 16:18:17 +08:00
上 redis 咯
|
50
leafre 2022-03-09 17:09:33 +08:00
处理好幂等即可
|
51
watcher 2022-03-09 17:23:58 +08:00
这个业务不存在高并发
|
52
codeMore 2022-03-09 17:26:15 +08:00
我感觉也是幂等问题,控制好订单生成就行了
|
53
daimubai 2022-03-09 17:37:59 +08:00
是幂等性问题,用乐观锁或者 redis
Boolean lock = redisLock.tryLock("key", "1", 10L, TimeUnit.SECONDS); if (lock) { try { //查库,调支付等 return "success"; } finally { redisLock.unlock(lockName, "1"); } } return "操作过于频繁,稍后再试!"; |
54
daimubai 2022-03-09 17:40:06 +08:00
高并发是指多个线程抢一个资源。
|
55
zzfer 2022-03-09 17:48:21 +08:00
up 可以看一下接口幂等校验
|
56
Nillouise 2022-03-09 17:54:20 +08:00
第一个线程查库,查到 flag 是 0
感觉不需要查,直接 update flag = 1 where flag=0 就可以了,跟 ksedz 说的思路一样。 另外,这里直接用数据库里的状态当锁不行,非要弄一个分布式锁的理由是什么?你请求的资源只涉及单个数据库,直接数据库内部操作就可以了,按我理解,分布式锁是锁住涉及多个数据库的资源,或者根本不是数据库的资源,比如一些对第三方接口的锁定。 |
57
lingalonely 2022-03-09 17:59:35 +08:00
你这不是单体的应用吧,如果是分布式的话,还高并发,直接用分布式锁最好,至于使用数据库作支持还是 redis ,zookeeper ,取决于系统当前有什么,不要随便引入新组件就好
|
58
littlewing 2022-03-09 18:00:07 +08:00
乐观锁 注意 ABA 问题
|
59
Amayadream 2022-03-09 18:01:51 +08:00
1.可以考虑合并未支付和支付中的状态,因为不论是什么状态,接到第三方状态都应该进行处理(自动退款或完成订单),多一个状态只能增加状态机的复杂度
2.这种场景下存在的问题是重复支付而不是高并发,这个需要从多维度共同解决,例如前端防重复支付(支付按钮防重复点击,从第三方支付跳转回来主动调用后端查询订单状态等),对后端来说最好拆分交易和支付订单,在一笔交易订单已经存在支付完成的支付单时直接返回支付成功的结果,一笔交易订单存在多笔成功的支付订单时(即重复支付)进行自动退款处理 |
60
DinnyXu 2022-03-09 19:44:15 +08:00
楼上各位大佬都已经补充了很多种实现方式,我来说下海量消息消费的时候这种场景 rocketmq 是怎么处理的。rocketmq 有个消费模式,是将 id 进行 hash ,相同的 id 一定是一个线程来操作的。
|
61
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; 根据结果再发送订单支付消息之类 |
62
teem 2022-03-09 22:37:55 +08:00
1 、不需要中间状态。
2 、支付动作(拉起收银台)只生成订单,订单的更新只根据回调修改 3 、支付回调全部进全局队列,一个一个操作。 |
63
documentzhangx66 2022-03-09 22:49:54 +08:00
年代不一样了,请别再用古老的阻塞方式,包括且不限于:悲观锁、行锁、表锁甚至库锁、分布式锁。
现代的处理方式是: 1.水平分库。电商业务,按用户进行分库,不同库跑在不同节点上,从源头上就减少了并发量。每个节点只处理一小部分用户的数据。 2.使用查询 + 增加数据版本的方式,来代替更新与删除。现代数据库应该尽量少地出现更新数据的操作,第一是为了保留历史数据,第二是现代设备是锁的同步代价远大于存储代价。 3.拥抱异常,把一单多付的场景视为正常情况考虑,做好出现这种情况后进行退款的流程与自动化即可。 |
64
Leviathann 2022-03-10 00:54:44 +08:00
@documentzhangx66 第 2 条,这样的话就把一个数据的所有历史版本都记录下来吗?
|
65
winglight2016 2022-03-10 07:41:00 +08:00
数据库别锁了,没有价值,#31 说的对,用户真的傻夫夫支付两次,只需要退一笔就可以了。业务流程不需要阻止什么操作,只要让流程走下去,而且在需要的时候可以回退就够了。
另外,楼上也说过,别搞第三种状态,只需要是否已支付两种状态就够了。 |
66
Jekins 2022-03-10 09:13:54 +08:00
@documentzhangx66 第二条所有历史版本都保存下来.请问后期如何统计订单数据 ?
|
67
undefine2020 2022-03-10 09:16:36 +08:00
没搞懂,为什么一个用户可以操作出高并发的情景出来
|
69
documentzhangx66 2022-03-10 12:18:35 +08:00
@Leviathann 是的
|
70
documentzhangx66 2022-03-10 12:20:02 +08:00
|
71
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 支付完成。 建议保留状态"支付中",该状态可以表示等待第三方回调,当请求发出去,第三方出现问题(超时、宕机)没有回调的时候,便于排查问题。 - 做好上面这些。再来考虑异常退款给用户的人工操作。因为即使代码上处理了,还会有服务器宕机、第三方平台问题等情况出现。生产问题总归是少不了的 :) |
73
sakasaka 2022-03-10 14:33:04 +08:00
分布式锁
|
74
seasonsolt 2022-03-10 14:40:59 +08:00 1
@documentzhangx66 本帖唯一高质量回答,和我们现在的处理方式比较接近,分布式 & lock free
1:首先订单系统根据用户请求做了 event 分发降低了并发量,数据库设计面向 event sourcing ,只增量记录 event log 2:重复支付发生时,订单实收 > 应收,追述到支付事件的 event log ,拿到支付凭证发起退款就可以了。 3:无论是前端锁还是后端锁,用户支付体验都会受影响,而且影响系统吞吐量 |
75
Chinsung 2022-03-10 17:17:41 +08:00
@seasonsolt #74 和你方案一样的就是高质量回答是吧,我比较好奇,你们这种做法
1. 如果用户重复支付了,会在用户端看到自己支付了多笔并且有几笔正在退款的信息吗? 2. 如果退款某笔失败了,你们是无限轮询还是手工处理,如果退款出现延时,难道用户就没有意见不会投诉吗? 3. 对账的话,如果跨清算时间退款了,对账复杂度也会比较高吧 一个用户 id+订单 id 加分布式锁的问题,对吞吐的影响真的有那么大? |
76
seasonsolt 2022-03-10 18:19:30 +08:00
@Chinsung 你的疑问全部理解,其实这个帖子里的我所谓的“不那么高质量”方案,我们都经历过,或者说这个行业都经历过,敢妄评“优劣”自然是建立无数的实践和采坑的基础之上的。 实践远比我的回答有意义,你可以去测试一些非 sdk 支付场景,支付成功后立马断网(飞行模式),或者杀掉支付 app ,切进程到商户收营台 再次支付。有些平台可以正常支付,但是很快自动退款,还有戏平台,会弹出报错 “您当前订单正在支付中,balabala.......” 。最后,做个统计,是不是实力更强的平台选择了 自动退款方案,比如 麦当劳、KFC...然后锁定单是不是相对弱一些(除了滴滴打车)
|
77
seasonsolt 2022-03-10 18:23:46 +08:00
@Chinsung 第三个问题确实是存在的,如果恰好退款时间恰好跨清算批次了,处理起来自然是恶心的,但是概率应该很小吧。 然后是纯技术的,lock free 更多算是一种技术洁癖吧,不一定能有多大性能损失,毕竟不是每一家都能做到 jd 、tb 的量级的。
|
78
9c04C5dO01Sw5DNL 2022-03-11 00:06:51 +08:00
|