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

users表:

ID 名称
1 约翰
2 David
3 新浪

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

ID 用户 金额 余额
1 约翰 +350 350
2 约翰 +250 600
3 约翰 -100 500
4 约翰 +400 900

通过查看上表,我们可以看到用户John的帐户余额(通过添加Amount列的值)是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个个

ID 用户 金额 余额
4 约翰 -500 400
5 David +500 500

目前还没有问题,但如果两个用户同时执行上述接口,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
ID 用户 金额 余额
1 约翰 +350 350
2 约翰 +250 600
3 约翰 -100 500
4 约翰 +400 900
5 约翰 -500 400
6 David +500 500
7 约翰 -600 300
8 新浪 +600 600

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

我想在Laravel年内防止这种情况发生.我能用的最好的方法是什么? (我的数据库是MySQL)

推荐答案

为什么这是一个问题

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

在这些从一个用户到另一个用户的交易中,您可能在一个步骤中完成了以下所有操作:

  1. 在一个账户中增加/扣除一笔金额
  2. 计算该帐户的余额
  3. 增加/扣除另一帐户中的金额
  4. 计算另一账户的余额

基本上,如果余额计算依赖于same table中的其他数据,如果允许该表在读取后更改,则在这里总是会遇到并发问题.

如何修复它?

要摆脱这一点,您需要将余额从插入事务中移开.

余额实际上是用户账户上所有交易的summary.因此,您应该总是能够通过对给定用户的所有交易进行SUM次计算来计算余额.这意味着你不应该在桌子上需要一个流动的平衡.

虽然这是真的,但每次添加新事务时,简单地对TRANSACTIONS表运行SUM查询是非常幼稚的,如果transactions表达到任何实际大小(如数百万行),则可能会导致性能非常差.

这就是锁之类的东西的用武之地,因为它有助于确保正在读取的值(在本例中为John的balance)在该过程中不会改变.

但是,为了从这种锁定中获得最大价值,您需要重新架构表,以使balance驻留在TRANSACTIONS表之外.

你的交易真的应该只是交易.

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

实际上,它应该在另一个名为accounts的表中,特别是如果这是在模仿一家银行,在那里用户可以有多个账户……但在这里让事情变得简单.

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

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

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

然后你可以阅读当前的balance,知道它不会改变!

$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])

现在,只有当John的余额足以支付款项时,才会处理扣减交易,并且只会更新John的余额.

所有这些都作为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 (100) and leave this as an exercise for the reader...

结果是

如果两个请求发生在同一时刻,这仍然不是MySQL的同一时刻.它将决定首先运行一个,锁定users表并运行事务,然后try 运行下一个.

当第二个运行时,它很可能会因为资金不足的例外而失败.这将允许您catch个异常,并给用户一个有用的消息.

Php相关问答推荐

WooCommerce:有条件地更改特定区域的发货类别

Laravel;Composer安装突然返回选项快捷方式不能为空.

将文件添加到存档,而不重建存档

根据WooCommerce购物车页面中的自定义 Select 添加费用

莫德克斯.当我try 使用pThumb为手机压缩图像时,没有任何react

在PHP中使用curl命令执行SCRAPYD操作.json不返回任何内容

根据WooCommerce Cart小计阈值自动应用优惠券

上传WordPress模板中的翻译文件(.mo和.po)

隐藏WooCommerce管理子菜单;主页;来自store 经理

如何在自定义邮箱内容中获取WooCommerce订单项目的详细信息?

Font Awesome 加载图标未显示在 WooCommerce 管理员列表中

用自定义代码替换 WooCommerce 单一产品简短描述

PHP访问子数组列表出错结果和长度

产品页面自定义复选框可对 WooCommerce 购物车小计启用百分比折扣

通过ajax发送数据到下拉列表未完全设置为所选

根据页面的最后修订日期设置 MediaWiki 内部链接的样式

Laravel 发送邮箱问题 Swift_TransportException

PHP 创建 CSV 文件并用波斯语写入

如何通过帖子自定义元数据进行查询

从图像URL中获取文件扩展名?