高并发下订单状态更新

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

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

7938 次点击
所在节点    Java
78 条回复
AS4694lAS4808
2022-03-09 11:24:26 +08:00
@wowbaby 昨天在美团定外卖,支付成功后不点完成,直接用返回按钮,好像会回到支付界面。。没有订阅银行卡或者支付工具通知的人,没准有可能再点一次付钱?
xiangyuecn
2022-03-09 11:28:38 +08:00
这个业务不存在高并发,用户不是机器人,一个订单最多并发两个请求就不错了,前端的问题
reeco
2022-03-09 11:31:20 +08:00
一锁二判三更新,悲观锁一把梭
HackerJax
2022-03-09 11:44:51 +08:00
这个不叫并发呀,一个订单只能一个人付款吧,这个应该算作请求去重
westoy
2022-03-09 11:46:07 +08:00
淘宝、京东、亚马逊、苹果碰到高并发抢购都会出现订单更新不及时、支付更新不及时(尤其像京东、淘宝大部分交易都是走的自己内部金融池子, 还不需要走银联)、掉单, 会通过限制一定周期内取消、更新订单以及客服和财务介入的方式去解决,你就别想通过程序一把梭哈了, 世界是不完美的
wushigejiajia01
2022-03-09 11:48:08 +08:00
@paradoxs

+1
确实是的

我们现在做的就是用 redis 分布式锁控制的
westoy
2022-03-09 11:56:12 +08:00
@westoy 限制一定周期内取消、更新订单 => 限制一定周期内禁止用户取消、更新订单

漏字歧义了....
timethinker
2022-03-09 12:01:27 +08:00
最好在数据库层面进行并发控制,不要在你的应用层加锁。直接加一个字段使用乐观锁来保证在并发的情况下只有一个事务会成功。并且你需要问自己一个很重要的问题,那就是这个业务真的会有大量的并发请求针对同一个订单进行操作吗?
yeyypp92
2022-03-09 12:03:13 +08:00
高并发下,前端应该也需要做一些限制,保证用户不会短时间内发起多次支付请求
shanghai1943
2022-03-09 12:05:56 +08:00
这种场景我一般是乐观锁 update xxx set xxxx=xx where id=xx and flag=0 谁能更新成功那谁就有机会执行往后的逻辑
Jooooooooo
2022-03-09 12:12:33 +08:00
重复支付退钱就行.
echooo0
2022-03-09 12:28:42 +08:00
前面几个说的很详细了,数据库行锁就可以搞定,这个应该不属于高并发,算是请求去重;

而且如果是单节点的话,用 synchronized ,也可以针对 id 对象加锁,不是一定要锁住所有线程
giiiiiithub
2022-03-09 12:35:21 +08:00
做成幂等接口就行吧
oneisall8955
2022-03-09 13:02:33 +08:00
题外话,我想问下,支付中这个状态是否有必要,多一个状态,就得维护多一个状态的情况。因为网络原因,支付失败,会不会存在永远都是 1 ,再也支付不了的情况?
frank1256
2022-03-09 13:05:40 +08:00
@oneisall8955 会存在,这种情况就是用户支付成功了,但是网络原因,支付系统没有通知过来。这个时候,需要主动发起查询动作,一般支付系统会给 2 个接口,一个是异步通知,一个是主动查询
frank1256
2022-03-09 13:06:16 +08:00
@echooo0 锁 id 对象?不太明白,是指 pojo 类的 id ,不是基本类型,而是一个对象吗?
frank1256
2022-03-09 13:07:38 +08:00
@shanghai1943 我现在的体量就很小,但是难免会遇到一点并发,所以就是想用乐观锁直接 update ,成功的才能往下走。我在想的是,这种操作本质上还是操作数据库了。能否减轻数据库的压力,减少那些会 update 失败的次数
summerLast
2022-03-09 13:11:10 +08:00
核心就是改成串行 java 相关的话 可以用 RedisLockRegistry 这个
summerLast
2022-03-09 13:14:45 +08:00
但是要注意 不要内部 方法调用 事务会失效 如 a.method(){this.dotran()}
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)

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

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

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

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

© 2021 V2EX