如何在Laravel Eloquent中处理多个并发更新?

时间:2018-01-16 08:29:25

标签: php laravel concurrency eloquent

Laravel 5.5

我想知道如何正确处理由不同用户或同一用户从不同页面对同一记录进行多次更新的可能情况。

例如,如果从数据库中读取Model_1的实例,响应来自Page_1的请求,并且响应来自Page_2的请求加载了同一对象的副本,那么如何最好地实现防止第二次更新的机制从破坏第一次更新? (当然,更新可以按任何顺序发生......)。

我不知道是否可以通过Eloquent锁定记录(我不想使用DB::进行锁定,因为您必须引用基础表和行ID),但即便如此如果可能的话,加载页面时锁定和提交时解锁也不合适(我将省略细节)。

我认为检测到先前的更新已经完成并且优雅地使后续更新失败将是最好的方法,但我是否必须手动执行此操作,例如通过测试时间戳(updated_at)字段?

(我假设Eloquent在更新之前不会自动比较所有字段,因为如果使用文本/二进制等大字段,这会有点低效)

2 个答案:

答案 0 :(得分:1)

你应该看看悲观锁定,这是一个阻止任何更新的功能,直到完成现有的更新。

查询构建器还包含一些函数,可帮助您对select语句执行“悲观锁定”。要使用“共享锁”运行语句,可以在查询中使用sharedLock方法。在您的事务提交之前,共享锁会阻止修改所选行:

DB::table('users')->where('votes', '>', 100)->sharedLock()->get();

或者,您可以使用lockForUpdate方法。 “for update”锁定可防止修改行或使用另一个共享锁选择:

DB::table('users')->where('votes', '>', 100)->lockForUpdate()->get();

参考:Laravel Documentation

答案 1 :(得分:1)

我想出的是:

<?php

namespace App\Traits;

use Illuminate\Support\Facades\DB;

trait UpdatableModelsTrait
{
    /**
     * Lock record for update, validate updated_at timestamp,
     * and return true if valid and updatable, throws otherwise.
     * Throws on error.
     *
     * @return bool
     */
    public function update_begin()
    {
        $result = false;
        $updated_at = DB::table($this->getTable())
            ->where($this->primaryKey, $this->getKey())
            ->sharedLock()
            ->value('updated_at');
        $updated_at = \Illuminate\Support\Carbon::createFromFormat('Y-m-d H:i:s', $updated_at);
        if($this->updated_at->eq($updated_at))
            $result = true;
        else
            abort(456, 'Concurrency Error: The original record has been altered');
        return $result;
    }

    /**
     * Save object, and return true if successful, false otherwise.
     * Throws on error.
     *
     * @return bool
     */
    public function update_end()
    {
        return parent::save();
    }

    /**
     * Save object after validating updated_at timestamp,
     * and return true if successful, false otherwise.
     * Throws on error.
     *
     * @return bool
     */
    public function save(array $options = [])
    {
        return $this->update_begin() && parent::save($options);
    }
}

用法示例:

try {
    DB::beginTransaction()
    $test1 = Test::where('label', 'Test 1')->first();
    $test2 = Test::where('label', 'Test 1')->first();
    $test1->label = 'Test 1a';
    $test1->save();
    $test2->label = 'Test 1b';
    $test2->save();
    DB::commit();
} catch(\Exception $x) {
    DB::rollback();
    throw $x;
}

由于时间戳不匹配,这将导致中止。

备注:

  • 只有存储引擎支持行锁时才能正常工作。 InnoDB确实。
  • 有一个开始和结束,因为您可能需要更新多个(可能是相关的)模型,并希望在尝试保存之前查看是否可以获取所有锁。另一种方法是简单地尝试在失败时保存和回滚。
  • 如果您愿意,可以使用闭包进行交易
  • 我知道自定义http响应(456)可能被视为不良做法,但您可以将其更改为返回false或throw,或500 ...
  • 如果您不喜欢特征,请将实施放在基础模型中
  • 必须改变原始代码才能使其自包含:如果您发现任何错误,请发表评论。