高并发下订单状态更新

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=某个条件。一定会有一个线程更新失败,更新成功的才会进行后续操作。这样的话,会对数据库有影响吗?

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

8354 次点击
所在节点    Java
78 条回复
xujihua
2022-03-09 10:44:01 +08:00
mysql 行锁 , 用主键或者唯一索引加锁 SELECT * FROM t1 WHERE c1 = (SELECT c1 FROM t2 FOR UPDATE) FOR UPDATE; 这种方案应该能解决你的问题
MoYi123
2022-03-09 10:44:27 +08:00
第三方支付不是一般会让你传一个订单号的吗? 至少支付宝是有的.
Canon1014
2022-03-09 10:44:33 +08:00
synchronized 可以根据业务的 id 上锁,搜索引擎关键字:synchronized id 上锁,当然单节点的前提是不变的
hcven
2022-03-09 10:44:35 +08:00
key+订单号,用 redis 的 incr 试一下?如果==1 则可以走支付中的逻辑,支付完成后写 redis 标志位再异步写 db 。>1 再去 redis 是不是已经支付完成?
yibo2018
2022-03-09 10:46:08 +08:00
用数据库的行锁,select ... where orderId = XXX for update 这样就能保证对于一个订单来说,只有一次请求可以获取锁
micean
2022-03-09 10:48:26 +08:00
flag 为 0 的时候,应该有唯一的订单号在微信 /支付宝那边阻止重复支付
frank1256
2022-03-09 10:51:47 +08:00
@MoYi123
@micean
我接过一些其他第三方支付,是第三方系统返回一个他们的流水号,我们调用方进行保存。这样就会导致订单会被发起 2 次支付
MoYi123
2022-03-09 10:55:32 +08:00
@frank1256 那就`update order set flag=1 where id = 'xxx' and flag = 0` ,
update 返回值是 1 的话发起支付, 和 cas 一个道理.
frank1256
2022-03-09 10:58:58 +08:00
@yibo2018
@xujihua
我了解一下
Chinsung
2022-03-09 11:05:59 +08:00
update 之前的时候加个分布式锁,获取到锁之后进去再 check 下状态是否是 1 ,是 1 直接报已在支付中了,否则 sql 带上 where flag=0 去更新下,根据更新成功行数判断是否需要发第三方支付
虽然你是同订单,其实并不存在高并发只存在并发,但是数据库锁最好少用,一个是得依赖唯一索引,另一个就是真高并发来了,数据库绝对会频繁报死锁
这种场景一般都是设计 2 张表,一张商品订单表,一张支付订单表,商品订单表改状态支付中直接就改了,扔个 mq 给支付订单表去生成支付订单,然后支付订单这里根据商品订单分布式锁做幂等就行了
timepast
2022-03-09 11:10:07 +08:00
两个层面的问题吧,
1. 如何保证状态一致性,单节点加锁、select ... for update , 分布式锁 等都能解决问题,实质是最小(业务)粒度的一致性,是排他的
2. 上面的问题解决了,订单生命周期,可以设计一个中间状态,即便是高并发,业务也应该有前提限制吧,已经有人支付中了,其他的请求应该失败重试,回调同理
paradoxs
2022-03-09 11:10:26 +08:00
分布式锁,是现在最优的解决方案。 现实里面,考虑到微服务集群,是不可能用 synchronized 之类的去解决的,没用。
frank1256
2022-03-09 11:11:31 +08:00
@Chinsung 感谢大佬解答
micean
2022-03-09 11:13:37 +08:00
锁数据库是没办法解决数据库以外的问题的,订单发起多次支付不需要处理,用户永远只会操作其中一次。假如真的出现了支付 2 次的情况,跑任务退款就行了
ksedz
2022-03-09 11:14:31 +08:00
update set flag = 1 where id = xxx and flag = 0;
然后检查更新的条数,为 1 才能继续。
waitfree 🐶
wowbaby
2022-03-09 11:15:49 +08:00
一个订单用户很少会重复支付吧,毕竟要付钱的,第三方支付提交的订单号不能重复支付的,至少我目前没遇到过这种问题
hidemyself
2022-03-09 11:17:56 +08:00
分布式锁,上 redis
frank1256
2022-03-09 11:19:14 +08:00
@wowbaby 这个就是我提的一个场景,本质上就是遇到“先查后写”的场景,如何保证并发下,不会出现问题
cheng6563
2022-03-09 11:20:39 +08:00
直接落库的业务就直接对数据库行加锁就行了。别搞太复杂。
k9982874
2022-03-09 11:24:14 +08:00
redis/etcd 加锁

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

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

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

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

© 2021 V2EX