如何防止用户两次取款?

时间:2014-09-14 23:56:53

标签: postgresql web-applications

我的api中有一个端点,可以让用户指定他们想要提取的金额。在我将提款请求发送给支付处理器之前,我检查所请求的金额是否<=用户的余额。处理完付款后,我会从用户的余额中扣除金额。

但是我想,有人可以在处理第一笔付款之前发送第二个请求,有效地撤回金额两次。我该如何预防这种情况?

PS:我正在使用Flask Restless和Postgres,如果这有任何区别的话。

1 个答案:

答案 0 :(得分:2)

在您与外部服务协调的情况下,这比您预期的要难。

繁体:准备交易和XA

此标准解决方案是使用两阶段提交,创建分布式事务,在发送付款请求之前更新用户的记录:

UPDATE account 
SET balance = balance - :requested_amount 
WHERE balance >= :requested_amount AND user_id = :userid`

如果更新成功(即他们有足够的钱),你PREPARE TRANSACTION让DB确认即使数据库崩溃也会保存tx。然后,您将请求发送给提供商,COMMIT PREPAREDROLLBACK PREPARED,具体取决于结果。

通过准备好的事务在余额记录上保持锁定,因此在准备好的tx回滚或提交之前不能开始其他tx,此时新余额可见。

旧的余额显示其他事务,直到准备好的事务提交或回滚,除非他们使用SELECT ... FOR UPDATESELECT ... FOR SHARE,在这种情况下,他们会等到准备好的TX提交/回滚。 NOWAIT选项可让他们错误输出。这一切都很方便。

但是,这种方法对于非常大的客户端数量而言难以扩展,如果支付处理器速度慢或无响应,则可能会出现问题。至少在PostgreSQL中,你一次可以拥有的准备交易数量有限制。

更具可扩展性:在应用程序中保留一个开放事务日志

如果您不想使用两阶段提交,则需要保留一个打开的事务日志。

而不仅仅是检查用户&#39;平衡,应用程序在active_transactions表中插入一行作为开始用户事务的一部分。如果用户已经有一个活动的交易,那么unique上需要active_transactions.user_id约束,所以如果有并发插入,则只有一个被拒绝。

您可能希望在同一交易中更新用户的余额。

其他方法,例如SELECTinsert记录之前检查用户,是不安全的,并且容易出现竞争条件。如果它们有助于提供更好的错误消息等,它们就可以了,但只能作为额外的检查接受。

然后您发送付款请求并等待回复。无论成功与否,当您收到回复时,您都会删除打开的交易日记帐分录并将其复制到历史记录表中;如果付款失败,您还将用户的余额重新添加到同一交易中的 ,然后提交。在您处理付款响应的同一交易中执行您需要的任何记录保存等。

无论哪种方式:交易清理

通过准备好的交易或应用程序定义的交易日记,现在您可以解决当您的应用/服务器因交易活跃而崩溃时您应该做什么以及您不知道支付处理器响应的内容对他们来说......或者你是否真的发送了请求。

大多数支付处理器API都允许您将应用程序定义的令牌附加到每个请求,从而为此提供一些帮助。如果您使用的是准备好的交易,则可以使用准备好的交易ID;如果您正在使用自己的交易日记帐,则可以使用在将条目插入交易日记帐时生成的ID。在崩溃/重新启动后重新启动时,您可以检查应用中的每个打开的事务,并询问支付处理器是否知道它,如果是,是否成功。

您还必须处理没有崩溃的情况,但由于网络短暂问题导致支付处理器请求或响应丢失等。您需要定期检查的代码对于明显放弃的开放交易,并使用支付处理器重新检查它们,如崩溃恢复后。

您需要处理多种故障模式:

  • 保存本地付款请求后,但在请求成功发送到处理器之前,应用程序崩溃/网络问题/等

  • 处理器关闭/无法访问

  • 处理器发送回复(付款失败/付款确定),但您的应用已关闭/无法访问,您永远无法收到回复。

  • 应用程序发送付款请求,然后在付款处理器完成处理请求(或完成接收请求)之前重新启动。清理代码认为处理器从未收到请求并丢弃本地交易记录,然后支付处理器响应以确认付款。 (有几种方法可以解决这个问题,但这个答案的范围真的不合适。)

  • ...更多?

欢乐时光,嗯?

有用的额外健全性检查是定期(例如,每天)从提供者处获取交易列表,并将其与您认为已完成的交易进行比较,确保完成状态匹配。标记人类评估的任何不匹配。它不应该发生,但...... ....