php - 防止 Laravel 中的用户帐户同时提款

标签 php mysql laravel transactions

我想防止帐户余额同时提取。 例如,当用户账户余额为500美元时不允许。两个提取 300 美元的并发请求会将用户余额更改为 -$100

用户表:

<表类=“s-表”> <标题> ID 姓名 <正文> 1 约翰 2 大卫 3 新浪

我有一个名为交易的表,如下所示:

<表类=“s-表”> <标题> ID 用户 金额 余额 <正文> 1 约翰 +350 350 2 约翰 +250 600 3 约翰 -100 500 4 约翰 +400 900

通过查看上表,我们可以看到用户 John 的帐户余额(通过添加金额列的值)为 $900

我创建了一个 API 将库存从一个用户转移到另一个用户

http://127.0.0.1:8000/api/v1/deposit/transfer/FromUser/ToUser/amount

如果我按如下方式运行 api 一次。交易表将发生变化,如您所见

http://127.0.0.1:8000/api/v1/deposit/transfer/John/David/500

<表类=“s-表”> <标题> ID 用户 金额 余额 <正文> 4 约翰 -500 400 5 大卫 +500 500

到目前为止我们没有问题。但是如果上面的api被两个用户同时执行,John的账户余额就会为负

假设该api被两个用户同时调用。发生以下情况

  1. http://127.0.0.1:8000/api/v1/deposit/transfer/John/David/500
  2. http://127.0.0.1:8000/api/v1/deposit/transfer/John/Sina/600
<表类=“s-表”> <标题> ID 用户 金额 余额 <正文> 1 约翰 +350 350 2 约翰 +250 600 3 约翰 -100 500 4 约翰 +400 900 5 约翰 -500 400 6 大卫 +500 500 7 约翰 -600 300 8 新浪 +600 600

现在约翰的总金额栏是多少? -100 美元,这是灾难的开始!

我想在 Laravel 中防止这种情况发生。我可以使用的最佳方法是什么? (我的数据库是mysql)

最佳答案

为什么这是一个问题

这里出现问题是因为一个事务不知道另一事务的结果余额是多少,并且它无法知道该问题的答案,因为它可能发生在单独的线程/进程和数据库连接中。

在这些“从一个用户到另一个用户”的交易中,您可能会在一个步骤中执行以下所有操作:

  1. 增加/减少一个账户中的金额
  2. 计算该帐户的余额
  3. 增加/减少另一个帐户中的金额
  4. 计算其他帐户的余额

基本上,如果余额计算依赖于同一表中的其他数据(如果该表在读取后允许更改),那么您总会遇到并发问题。

如何修复

要避免这种情况,您需要将余额从交易插入中移开。

余额实际上是用户帐户上所有交易的摘要。因此,您应该始终能够简单地通过对给定用户的所有交易进行SUM计算余额。这意味着您不需要在表中添加运行余额。

虽然确实如此,但每次添加新交易时仅对交易表运行 SUM 查询是非常幼稚的,并且如果您的 交易 表可以达到任何实际大小(想想数百万行)。

这就是锁之类的东西的用武之地,因为它有助于确保正在读取的值(在本例中为 John 的余额)在此过程中不会发生变化。

为了充分利用这种锁定的值(value),您需要重新构建表,以便余额位于事务表之外。

您的交易实际上应该只是交易。

如果您在其他地方保持运行平衡,您还将拥有一个更简单的数据集,因为这可能只是每个用户的单个值。例如,您可以将其放入 users 表中。

实际上,它应该位于另一个名为 accounts 的表或其他表中,特别是如果这是模仿银行,用户可以拥有多个帐户......但这里保持简单。

然后,当您收到从一个帐户扣除的请求时,您将获得 users 表上的锁定(有关更多详细信息,请参阅 Pessimistic Locking 上的文档),以确保 余额不会首先被任何其他交易更改。

锁作为数据库事务的一部分发生在 MySQL 级别,并且实际上阻止任何其他请求编辑该行。这意味着在处理此交易时传入的任何其他交易都将被阻止。

$john = DB::table('users')
    ->where('user', 'John')
    ->sharedLock()
    ->get()
    ->sole();

然后您可以读取当前的余额,知道它不会改变!

$balance = $john->balance;

然后添加新交易并更新 John 的余额:

$amount = -500; // Obviously variable, coming from your request

if ($amount < 0 && $john->balance < -$amount) {
    throw new \Exception('Insufficient funds');
}

DB::insert('insert into transactions (user, amount) values (?, ?)', ['John', $amount]);

$new_balance = $john->balance + $amount;

DB::update('update users set balance = ? where user = "John"', [$new_balance])

现在,只有当约翰的余额足以支付付款时,才会处理扣除交易,并且约翰的余额才会更新。

将这一切作为 database transaction 的一部分进行一旦事务提交,users表将自动解锁。现在大家在一起...

use Illuminate\Support\Facades\DB;
 
DB::transaction(function () use ($amount, $from_user, $to_user) {
    $from = DB::table('users')
        ->where('user', $from_user)
        ->sharedLock()
        ->get()
        ->sole();

    if ($amount < 0 && $from->balance < -$amount) {
        throw new \Exception('Insufficient funds');
    }

    DB::insert('insert into transactions (user, amount) values (?, ?)', [$from_user, $amount]);

    $new_balance = $from->balance + $amount;

    DB::update('update users set balance = ? where user = ?', [$from_user, $new_balance])
}, 5);

NB: For brevity, I've skipped the parts that relate to the other user ($to_user) and leave this as an exercise for the reader...

结果

如果两个请求在完全相同的时刻发生,对于 MySQL 来说这仍然不是完全相同的时刻。它将决定先运行一个,锁定users表并运行事务,然后尝试运行下一个。

当第二个运行时,很可能会因为资金不足异常而失败。这将使您能够捕获该异常并向用户提供有用的消息。

关于php - 防止 Laravel 中的用户帐户同时提款,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/77341493/

相关文章:

php - 如何从 Controller 访问 Zend Framework 应用程序的配置?

php - 具有多个条件的 OData 查询 $filter

mysql - 我应该计数(*)还是不?

php - MySQL 1064 更改表错误

node.js - Laravel 混合 : ValidationError: CSS Loader has been initialized using an options object that does not match the API schema

php - 使用 php 变量创建动态 mysql 查询

php - 在 mysql 上使用 SQL_SAFE_UPDATES

javascript - 无法通过 ajax 将 attr int 变量发送到 php,到 db 列

php - Laravel 5.7 - 为什么以编程方式列出控制台命令返回 0?

php - 如何在 Laravel 中获得最畅销的产品?