简体   繁体   中英

How to handle multiple concurrent updates in Laravel Eloquent?

Laravel 5.5

I'm wondering how to properly handle the possible case of multiple updates to the same records by separate users or from different pages by the same user.

For example, if an instance of Model_1 is read from the database responding to a request from Page_1, and a copy of the same object is loaded responding to a request from Page_2, how best to implement a mechanism to prevent a second update from clobbering the first update? (Of course, the updates could occur in any order...).

I don't know if it is possible to lock records through Eloquent (I don't want to use DB:: for locking as you'd have to refer to the underlying tables and row ids), but even if it were possible, Locking when loading the page and unlocking when submitting wouldn't be proper either (I'm going to omit details).

I think detecting that a previous update has been made and failing the subsequent updates gracefully would be the best approach, but do I have to do it manually, for example by testing a timestamp (updated_at) field?

(I'm supposing Eloquent doesn't automatically compare all fields before updating, as this would be somewhat inefficient, if using large fields such as text/binary)

You should take a look at pessimistic locking, is a feature that prevents any update until the existing one its done.

The query builder also includes a few functions to help you do "pessimistic locking" on your select statements. To run the statement with a "shared lock", you may use the sharedLock method on a query. A shared lock prevents the selected rows from being modified until your transaction commits:

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

Alternatively, you may use the lockForUpdate method. A "for update" lock prevents the rows from being modified or from being selected with another shared lock:

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

Reference: Laravel Documentation

What I came up with was this:

<?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);
    }
}

Usage example:

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;
}

This will cause abort as the timestamp does not match.

Notes:

  • This will only work properly if the storage engine supports row-locks. InnoDB does.
  • There is a begin and an end because you may need to update multiple (possibly related) models, and wish to see if locks can be acquired on all before trying to save. An alternative is to simply try to save and rollback on failure.
  • If you prefer, you could use a closure for the transaction
  • I'm aware that the custom http response (456) may be considered a bad practice, but you can change that to a return false or a throw, or a 500...
  • If you don't like traits, put the implementation in a base model
  • Had to alter from the original code to make it self contained: If you find any errors, please comment.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM