Laravel数据库事务会锁定表吗?

25
我在在线支付应用中使用laravel5.5的数据库事务。我有一个company_account表来记录每一笔付款(类型,金额,创建时间,总收入)。当创建新记录时,我需要访问上一条记录的总收入(gross_income)。因此,我需要在事务中使用读写表锁(lock),以避免多笔付款同时进行。
我参考了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);

参考资料:
如何向Laravel DB::transaction()传递参数


2
事务和锁是为不同目的而构建的两个不同的东西。您不需要锁定,也不需要限制一次只能进行一次付款。请阅读 https://dev59.com/SG855IYBdhLWcg3wpmDw 等网站上的示例,了解事务和回滚的工作原理。 - Alex Blex
@AlexBlex 您的链接很有帮助。但我认为我需要同时使用锁(读写)和事务,在事务中,我将访问最后一条记录的gross_income,然后计算新记录的当前gross_income。我在这里走错了吗? - LF00
1
我找到了一个可能值得讨论的答案,https://dev59.com/-3A75IYBdhLWcg3wy8Mi。它解释了在事务进行中没有其他用户更改的保证。 - Dharma Saputra
如果在同一张表中,很可能既不需要锁也不需要事务。一个单独的原子更新就足够了。如果您更新问题并提供表模式和尝试在事务中执行的查询,我很乐意给出完整的答案和示例。 - Alex Blex
1
@AlexBlex 我已经更新了我的问题,并附上了代码示例。 - LF00
3个回答

13

由于您正在更新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);
有两个原子查询。第一个将记录插入到“user_account”表中,另一个将记录插入到“tiger_account”表中。
你需要使用事务来确保在这两个查询之间发生可怕的事情时不会应用任何更改。可怕的事情不是并发请求,而是PHP应用程序的突然死亡、网络分区或其他阻止执行第二个查询的事件。在这种情况下,来自第一个查询的更改将被回滚,因此数据库保持一致状态。
这两个查询都是原子性的,这保证了每个查询中的运算都是独立的,并且在此期间没有其他查询更改表。可以说,2个并发请求同时处理同一用户的2个付款是可能的。第一个请求将在“user_account”表中插入或更新记录,第二个查询将更新该记录,两个查询都将向“tiger_account”添加一条记录,当每个事务提交时,所有更改都将永久地保存在db中。
我做出的一些假设:
- “user_id”是“user_account”表中的主键。 - “tiger_account”中至少有1条记录。在原始代码中称为“$old_tiger_account”,因为不清楚在db中什么是预期行为。 - 所有货币字段都是整数,而不是浮点数。 - 这是MySQL DB。我使用MySQL语法来说明这种方法。其他SQL风味可能有稍微不同的语法。 - 所有表名和列名都采用原始查询中的名称。不记得阐述命名约定。 警告一句。这些是原始查询。您应该特别注意未来重构模型,并编写更多的集成测试,因为一些应用程序逻辑已从命令式PHP转移到声明性SQL。我相信这是为了保证没有竞争条件而付出的公平代价,但我想让它变得非常清楚,这并不是免费的。

谢谢回答,但为什么LF00未来需要重构模型呢? - reza
1
@reza 因为事物会发生变化,软件需要跟上。即使是像 cat 这样基本的程序,在服役近50年后也已经更新到第8个版本了。而 SQL 模型的衰减速度要快得多。 - Alex Blex

8

我看到了这个答案,解释了事务和锁定表的问题MySQL:事务 vs 锁定表。它显示在这里都应该使用事务和锁定。

我参考了Laravel lockforupdate(悲观锁定)如何将参数传递给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);

1
是的,这应该可以工作。锁定整个表不是特别高效,但它确切地做了你在问题中提到的要求。我能看到的唯一好处是你避免使用原始查询,这可能会弥补性能损失。 - Alex Blex

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计算错误。 - LF00
1
@KrisRoofe,根据您的代码,每个记录的总收入是其费用加上前一个记录的总收入。这意味着每个记录的总收入等于当前记录之前所有费用的总和。如果是这样的话,请再看一下我的答案,您实际上不需要为读取锁定表。此外,为读取而锁定表确实会影响性能。如果您计划启动中型或大型服务,您永远不应该为重复操作锁定表(特别是读取锁)。总有替代方案。 - Vahid
@ 我在这里同意您的观点。 - LF00

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