我应该使用队列系统来处理支付吗?

4
我正在使用SlimStripe的PHP库一起处理我的应用程序中的支付。一切都很好,但最近我发现了一个令人震惊的故障,我认为这可能比我想象的要严重得多。在我的逻辑中,在支付过程的三个不同检查点处,我会检查我的(MYSQL)数据库中的库存,以确保用户未购买超出可用量的产品。
然而,当多个用户在大约500毫秒内请求时,支付系统似乎会同时处理这些请求,导致一系列问题,包括库存不正确和不平衡,以及虚假用户确认成功付款等。
通过一些尽职调查,我将解决方案缩小到两个选项(尽管我可能卖了自己的短):
1)使用队列系统,根据我的理解,将排队这些请求,并一次处理它们,创建一种先来先服务的基础。
2)在每个请求上附加一些中间件,充当队列,并尝试同步处理每个请求(虽然这可能类似于已经存在的内容)
现在,对于这些选项有什么建议/意见?当然,如果有需要,随时完全放弃我的理念并指引我走向不同的方向。

我肯定认为一个带有实时确认请求已被处理的队列系统是完全有效的方式。如果我有你的顾虑,我会选择一个与Websockets相结合的队列系统,告诉我的前端后端已经从头到尾处理了请求。这也给你带来了很多很酷的好处--重试、响应速度、比特币等异步处理。Stripe也是同步的,所以你可以在处理付款后进行正常的清理,以确保你的库存是正确的。 - korben
你应该考虑使用事务处理。同时,确保你的数据库被正确索引,并可能拥有(另一个)唯一索引。你使用哪个关系型数据库管理系统?记得 @Niner 我。 - Funk Forty Niner
@JaredGarcia,可以看一下https://dev.mysql.com/doc/refman/5.7/en/commit.html和www.mysqltutorial.org/mysql-transaction.aspx,这会让你更好地理解我所说的内容。此外,如果你的数据库没有正确建立索引,或者没有某些形式的唯一索引,那么这可能是问题的原因。即使有毫秒级别的差异,也不应该存在冲突或失败的情况。 - Funk Forty Niner
@JaredGarcia 嗯,使用 UNIQUE 约束不会有任何问题。如果你的数据库被查询而不是其中一个主键/自增列,那么这可能会产生某些副作用。在这里,事务可能是解决方案。 - Funk Forty Niner
如果您认为这个问题值得回答(由我来回答)或者应该标记为可能是 https://dev59.com/KXE95IYBdhLWcg3wUMPW 的重复问题,请告诉我。您也可以查看那个问答;这个问题似乎也是一个竞态条件。 - Funk Forty Niner
显示剩余8条评论
5个回答

1

如果我理解正确,你的主要问题是担心有人购买已经售出的商品(此时正在处理付款)...

我认为你应该放弃排队系统的想法——因为这不是关键所在。关键在于你的商店逻辑。

我不知道你的商店如何运作,但从逻辑角度来看,在客户点击发送订单时,产品应该被锁定(分配),而不是在付款过程之后。因为并不是每个付款都可能成功(如果客户想用stripe或其他支付方式重试失败的付款怎么办?)。


我明白了,我的商店逻辑非常简单。就像我说的那样,我会在三个不同的检查点检查可用性:1)当用户选择结账按钮时,会发送一个Ajax请求来检查可用性;2)成功输入付款凭据后;3)在请求到达Stripe API之前。 - Jared Garcia
我不会锁定产品,直到通过 Stripe 成功捕获付款,如果没有成功,用户可以重试。 - Jared Garcia
1
我同意@Michal G的观点 - 你必须将该项目标记为“已预订”,并将其与付款信息连接起来。考虑一个时间戳“保留直到”,并在SQL中进行检查。如果从未进行付款,则锁定会自动超时。只需确保一旦付款完成,将其标记为“已售出”。 - Honk der Hase
@Lars Stegelitz 我理解这种方法,但是这不会遇到多个用户同时锁定同一件物品并可能成功同时结账的问题吗?这些物品可能是不可用的。 - Jared Garcia
1
如果锁定实现得当,就不会出现这种情况。"互斥锁"、"信号量"或"临界区"是专门设计用来处理这种情况(竞态条件)的。对于常见的数据库,您可以自己实现一些东西(一个带有唯一键的表格用于该部分。创建记录->您拥有该锁。如果创建失败->其他人持有该部分的锁。只有在获得锁之后才能采取行动。释放锁->删除记录)。我曾为我正在开发的浏览器游戏实现了类似的东西,因为我们经常遇到这些问题。 - Honk der Hase
哇,这听起来像是一个有趣的实现。你介意私聊吗?我不是新手,所以我保证这不会成为负担,只是想进一步讨论技术并展示一些代码@Lars Stegelitz。 - Jared Garcia

0

我听说过很多关于开发人员使用排队系统的恐怖故事,所以我绝对不建议这样做。先到先服务的原则经常会导致难以捕捉的错误,直到代码放置后才能发现 - 我认识的一个开发人员使用类似的系统创建了一个商店,结果发现许多客户购物车里有随机商品或者在付款结束时发现没有库存。

相反,我会使用jQuery或Javascript或PHP库来使用“事件监听器”,以确保在结账过程中,如果数据库中的某些内容发生变化,则UI将向用户指示库存已更改或不可用。


我明白了,我的商店逻辑相当简单。就像我说的那样,我会在三个不同的检查点检查可用性:1)当用户选择结账按钮时,会发送一个Ajax请求来检查可用性;2)成功输入付款凭据后;3)在请求到达Stripe API之前。 - Jared Garcia
这对于你的应用程序来说最初肯定是有效的,但是可扩展性可能会成为未来的一个巨大问题。即使它稍微降低了性能,也值得拥有始终等待数据库更改的东西。因为正如你所发现的那样,在检查点之间股票可能随时发生变化。 - Perniferous

0

我认为你的问题与支付提供商无关,而是与你的商店逻辑有关。 我会按照以下步骤进行:

  1. 用户点击要添加到购物车的产品
  2. 产品进入“结账中”列表,并且您商店中可用数量减少一个
  3. 然后我们有两种可能性:
    • 用户完成带有成功付款的结账 => 您可以保留您产品的总金额
    • 用户中止结账 => 在特定时间(例如5分钟)后,您必须增加您产品的总金额

如果您尝试在电影院购买座位,则必须在5分钟内完成结账(倒计时显示给用户),否则预订将失效,其他人可以预订座位。


这是一个很好的方法,但如果只有5个座位,而两个用户正在同时购买所有5个座位,那么这是否仍会导致某种摩擦? - Jared Garcia
当然可以。但是不知道商店销售的产品类型,我无法找到解决方法。您当然可以向第二个用户展示有一些座位被保留,将在5分钟后释放,或者缩短预订时间。 - Sebastian

0

我想建议另一种处理方式,这是我们在商店中处理付款和库存的方式。主要问题在于高销售量期间,我们使用库存管理解决了黑色星期五和网络星期一的销售问题。

您必须确保遵循以下并发支付步骤

  1. 在用户即将进入付款流程之前,应立即检查可用库存。您可以通过将此数据保存在Redis中而不是每次查询数据库来完成此操作。每当下订单时,它都会更新Redis中的库存条目以保持同步。
  2. 当您即将为付款调用付款网关时,请检查可用库存并在可用时继续处理,否则提示用户。
  3. 当用户完成付款后,使用锁定从数据库交易中扣除购买数量。如果交易未成功,则退款或者您可以选择是否为缺货商品下订单。

希望这可以帮助您。


0

使用TRANSACTIONS对于这个问题会很有益,因为你似乎面临了所谓的竞态条件。

引用自https://www.w3resource.com/mysql/mysql-transaction.php

事务是包含一个或多个SQL语句的逻辑工作单元。事务是原子工作单元,可以提交或回滚。当事务对数据库进行多个更改时,当事务提交时,所有更改都成功,或者当事务回滚时,所有更改都被撤消。

事务从第一个可执行的SQL语句开始。当使用COMMIT或ROLLBACK语句显式提交或回滚事务时,事务结束,或者在发出DDL(数据定义语言(DDL)用于管理表和索引结构,CREATE、ALTER、RENAME、DROP和TRUNCATE语句是一些数据定义元素的名称)语句时隐式结束。

在事务期间锁定表格将有助于防止竞态条件。

同样来自该资源:

LOCK TABLES      
  tbl_name [[AS] alias] lock_type      
  [, tbl_name [[AS] alias] lock_type] ...    

lock_type:      
  READ [LOCAL]    
 | [LOW_PRIORITY] WRITE    

UNLOCK TABLES

更多信息和语法可以在(官方)MySQL网站上找到:

"关于事务",摘自https://www.informit.com/articles/article.aspx?p=29312

事务是一组按顺序执行的数据库操作,就像是一个单独的工作单元。换句话说,只有在组内的每个操作都成功完成时,事务才算完成。如果事务中的任何操作失败,整个事务都将失败。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接