【问题标题】:Will laravel database transaction lock table?请问laravel数据库事务锁表吗?
【发布时间】:2018-04-13 13:48:58
【问题描述】:

我使用 laravel5.5 的数据库事务进行在线支付应用。我有一个 company_account 表来记录每次付款(typeamountcreate_atgross_income)。创建新记录时,我需要访问最后一条记录的gross_income。所以我需要在事务的时候用读写表锁来锁表,以避免同时进行多次支付。

我参考了 laravel 的文档,但我不确定事务是否会锁定表。如果事务会锁表,锁的类型是什么(读锁、写锁或两者兼有)?

DB::transaction(function () {
    // create company_account record

    // create use_account record
}, 5);

代码:

DB::transaction(function ($model) use($model) {
    $model = Model::find($order->product_id);
    $user = $model->user;

    // **update** use_account record
    try {
        $user_account = User_account::find($user->id);
    } catch (Exception $e){
        $user_account = new User_account;
        $user_account->user_id  = $user->id;
        $user_account->earnings = 0;
        $user_account->balance  = 0;
    }
    $user_account->earnings += $order->fee * self::USER_COMMISION_RATIO;
    $user_account->balance += $order->fee * self::USER_COMMISION_RATIO;
    $user_account->save();

    // **create** company_account record
    $old_tiger_account = Tiger_account::latest('id')->first();

    $tiger_account = new Tiger_account;
    $tiger_account->type = 'model';
    $tiger_account->order_id = $order->id;
    $tiger_account->user_id = $user->id;
    $tiger_account->profit = $order->fee;
    $tiger_account->payment = 0;
    $tiger_account->gross_income = $old_tiger_account-> gross_income + $order->fee;
    $tiger_account->save();
}, 5);

参考:
How to pass parameter to Laravel DB::transaction()

【问题讨论】:

  • 事务和锁是为不同目的而构建的两种不同的东西。您不需要锁定,也不需要限制一次付款。请阅读stackoverflow.com/questions/4226766/… 和其他类似内容,了解事务和回滚如何工作的示例。
  • @AlexBlex 您的链接很有帮助。但我认为我需要同时使用锁定(读取和写入)和事务,因为在事务中我将访问最后一条记录的总收入,然后计算新记录的当前总收入。我是不是走错路了。
  • 我找到了一个值得讨论的答案,stackoverflow.com/questions/3106737/…。说明我们在交易过程中保证没有其他用户更改。
  • 如果它在同一个表中,很可能你既不需要锁也不需要事务。单个原子更新就足够了。如果您使用表架构和您尝试在事务中执行的查询更新问题,很高兴通过示例给出完整的答案。
  • @AlexBlex 我已经用代码示例更新了我的问题。

标签: php database laravel transactions table-locking


【解决方案1】:

由于您要更新 2 个表,因此您仍然需要使用事务来保持更改同步。考虑以下代码:

DB::transaction(function () {
    $model = Model::find($order->product_id);
    $user = $model->user();

    DB::insert("
        insert into user_account (user_id, earnings, balance) values (?, ?, ?)
        on duplicate key update
        earnings = earnings + values(earnings),
        balance = balance + values(balance)
    ", [$user->id, $order->fee * self::USER_COMMISION_RATIO, $order->fee * self::USER_COMMISION_RATIO]);

    DB::insert(sprintf("
        insert into tiger_account (`type`, order_id, user_id, profit, payment, gross_income)
            select '%s' as `type`, %d as order_id, %d as user_id, %d as profit, %d as payment, gross_income + %d as gross_income
            from tiger_account
            order by id desc
            limit 1
    ", "model", $order->id, $user->id, $order->fee, 0, $order->fee));

}, 5);

有 2 个原子查询。第一个将记录插入user_account 表,另一个将记录插入tiger_account

如果这两个查询之间发生了可怕的事情,您需要事务来保证不会应用任何更改。可怕的不是并发请求,而是 php 应用程序、网络分区或任何其他阻止执行第二个查询的突然死亡。在这种情况下,从第一个查询回滚的更改,因此数据库保持一致状态。

这两个查询都是原子的,这保证了每个查询中的数学运算都是独立完成的,此时没有其他查询会更改表。说有可能 2 个并发请求同时为同一用户处理 2 笔付款。第一个将在user_account 表中插入或更新一条记录,第二个查询将更新该记录,两者都将向tiger_account 添加一条记录,并且在提交每个事务时,所有更改都将永久设置在数据库中。

我做了几个假设:

  • user_iduser_account 表中的主键。
  • tiger_account 中至少有 1 条记录。在 OP 代码中称为 $old_tiger_account 的那个,因为当数据库中没有任何内容时,不清楚预期的行为是什么。
  • 所有货币字段都是整数,而不是浮点数。
  • 它是 MySQL 数据库。我使用 MySQL 语法来说明该方法。其他 SQL 风格的语法可能略有不同。
  • 原始查询中的所有表名和列名。不要记得阐明命名约定。

一句警告。这些是原始查询。随着一些应用程序逻辑从命令式 PHP 转移到声明式 SQL,您将来应该特别注意重构模型,并编写更多的集成测试。我认为保证没有比赛条件是一个公平的价格,但我想明确表示它不是免费的。

【讨论】:

  • 感谢解答...,但是为什么LF00以后还需要重构模型呢?
  • @reza 因为事情发生了变化,软件需要跟上。即使像cat 这样的基本程序在使用了近 50 年后也达到了第 8 版。 SQL 模型往往衰减得更快。
【解决方案2】:

我遇到了问题MySQL: Transactions vs Locking Tables 的这个answer,它解释了事务和锁定表。它显示了这里应该使用的事务和锁定。

我参考Laravel lockforupdate (Pessimistic Locking)How to pass parameter to Laravel DB::transaction(),然后得到下面的代码。

我不知道它是否是一个很好的实现,至少它现在可以工作了。

DB::transaction(function ($order) use($order) {
    if($order->product_name == 'model')
    {
        $model = Model::find($order->product_id);
        $user = $model->user;

        $user_account = User_account::where('user_id', $user->id)->lockForUpdate()->first();

        if(!$user_account)
        {
            $user_account = new User_account;
            $user_account->user_id  = $user->id;
            $user_account->earnings = 0;
            $user_account->balance  = 0;
        }

        $user_account->earnings += $order->fee * self::USER_COMMISION_RATIO;
        $user_account->balance += $order->fee * self::USER_COMMISION_RATIO;
        $user_account->save();

        $old_tiger_account = Tiger_account::latest('id')->lockForUpdate()->first();
        $tiger_account = new Tiger_account;
        $tiger_account->type = 'model';
        $tiger_account->order_id = $order->id;
        $tiger_account->user_id = $user->id;
        $tiger_account->profit = $order->fee;              
        $tiger_account->payment = 0;

        if($old_tiger_account)
        {
            $tiger_account->gross_income = $old_tiger_account->gross_income + $order->fee;
        } else{
            $tiger_account->gross_income = $order->fee;
        }

        $tiger_account->save();
    }
}, 3);

【讨论】:

  • 是的,这应该可以。锁定整个表并不是特别高效,但它完全符合您在问题中提出的要求。我看到的唯一好处是您远离原始查询,这可以证明性能损失是合理的。
【解决方案3】:

在我看来,如果你单独计算每条记录的总收入,你甚至不需要锁定表格,你知道锁定表格会直接降低你的网站速度。

DB::transaction(function () use($order) {
    $model = Model::find($order->product_id);
    $user = $model->user;

    // **update** use_account record
    try {
        $user_account = User_account::find($user->id);
    } catch (Exception $e){
        $user_account = new User_account;
        $user_account->user_id  = $user->id;
        $user_account->earnings = 0;
        $user_account->balance  = 0;
    }
    $user_account->earnings += $order->fee * self::USER_COMMISION_RATIO;
    $user_account->balance += $order->fee * self::USER_COMMISION_RATIO;
    $user_account->save();

    // **create** company_account record
    $tiger_account = Tiger_account::create([
        'type' => 'model',
        'order_id' => $order->id,
        'user_id' => $user->id,
        'profit' => $order->fee,
        'payment' => 0,
    ]);

    $tiger_account->update([
        'gross_income' => Tiger_account::where('id', '<=', $tiger_account->id)->sum('fee'),
    ]);
});

【讨论】:

  • 我应该访问最后一条记录的gross_income 列来计算新记录。如果我不锁桌,可能会有多笔同时付款。这将导致错误的gross_income 计算。
  • @KrisRoofe 正如我在您的代码中发现的那样,每条记录的总收入是它的费用 + 以前记录的总收入。所以这意味着每条记录的总收入=当前记录的所有费用的总和。如果它是正确的,那么再看看我的答案,你真的不需要锁定表来阅读。此外,锁定表以供读取确实是一个性能问题。如果您计划启动中型或大型服务,则永远不应锁定表(特别是读锁)以进行重复操作。总有替代解决方案。
  • @我同意你的观点。
猜你喜欢
  • 2011-01-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-04-07
  • 1970-01-01
  • 2022-01-19
相关资源
最近更新 更多