xint0/laravel-optimistic-locking

为Eloquent模型添加乐观锁功能。

1.3.5 2024-06-28 13:22 UTC

This package is auto-updated.

Last update: 2024-08-29 17:35:32 UTC


README

GitLab (self-managed) Packagist PHP Version Packagist PHP Version Gitlab pipeline status (self-managed) Gitlab code coverage (self-managed, specific job) GitLab last commit Packagist Downloads (custom server) Packagist Version (custom server)

为Eloquent模型添加乐观锁。

此包替换了 reshadman/laravel-optimistic-locking

安装

composer require xint0/laravel-optimistic-locking

兼容性

此包支持Laravel 5.5、5.6、5.7、5.8、6、7、8、9、10和11。

用法

基本用法

在你的模型中使用 \Reshadman\OptimisticLocking\OptimisticLocking 特性

<?php

use Reshadman\OptimisticLocking\OptimisticLocking;

class BlogPost extends Model {
    use OptimisticLocking;
}

将可空的整数 lock_version 字段添加到模型的表中

<?php # migration to add lock version column

$schema->integer('lock_version')->unsigned()->nullable();

然后你就可以开始了,如果两个不同的进程 并发地 编辑了相同的资源,则会抛出以下异常

<?php

use Reshadman\OptimisticLocking\StaleModelLockingException;

你应该捕获上述异常,并根据你的业务逻辑正确处理。

在业务事务中维护lock_version

你可以在业务事务中跟踪锁版本,通过通知你的API或HTML客户端当前的版本

<input type="hidden" name="lock_version" value="{{$blogPost->lock_version}}" 

并在控制器中

<?php

use Reshadman\OptimisticLocking\StaleModelLockingException;
// Explicitly setting the lock version
class PostController {
    public function update($id)
    {
        $post = Post::findOrFail($id);
        $post->lock_version = request('lock_version');
        try {
            $post->save();
            // You can also define more implicit reusable methods in your model like Model::saveWithVersion(...$args); 
            // or just override the default Model::save(...$args); method which accepts $options
            // Then automatically read the lock version from Request and set into the model.
        } catch (StaleModelLockingException $exception) {
            // Occurs when request lock version does not match value stored in database. Which means another user
            // has already updated the model.
        }
    }
}

因此,如果有两个作者同时编辑相同的内容,你可以跟踪你的 读取状态,并要求第二个作者重写他的更改。

禁用和启用乐观锁

你可以禁用和启用特定实例的乐观锁

<?php
$blogPost->disableLocking();
//
//save model does not verify lock version
//
$blogPost->enableLocking();
//
// save model verifies lock version
//

默认情况下,当你使用 OptimisticLocking 特性在你的模型中时,乐观锁是启用的,为了改变默认行为,你可以将锁定严格设置为 false

<?php

use Illuminate\Database\Eloquent\Model;
use Reshadman\OptimisticLocking\OptimisticLocking;

class BlogPost extends Model 
{
    use OptimisticLocking;
    
    protected $lock = false;
}

然后你可以启用它: $blogPost->enableLocking();

使用不同的列来跟踪版本

默认情况下,使用 lock_version 列来跟踪版本,你可以通过重写特性的以下方法来改变这一点

<?php

use Illuminate\Database\Eloquent\Model;
use Reshadman\OptimisticLocking\OptimisticLocking;

class BlogPost extends Model
{
    use OptimisticLocking;
    
    /**
     * Name of the lock version column.
     *
     * @return string
     */
    protected static function lockVersionColumn()
    {
        return 'track_version';
    }
}

什么是乐观锁?

有关详细说明,请阅读 Martin Fowler的《企业应用架构模式》 的并发部分。

有两种方法可以解决通用的并发竞争条件

  1. 不允许其他进程(或用户)读取和更新同一资源(悲观锁)。
  2. 允许其他进程并发读取同一资源,但如果在其中一个进程更新资源之前其他进程已经更新了资源,则不允许进一步更新(乐观锁)。

Laravel允许如文档所述的悲观锁,此包允许你以类似Rails的方式拥有乐观锁。

乐观锁期间发生了什么?

每次你对资源(模型)执行更新操作时,表中的 lock_version 计数器字段都会增加 1。如果你读取了一个资源,而另一个进程在你在读取之后更新了该资源,则真实版本计数器增加一个。如果当前进程尝试更新模型,则会抛出 StaleModelLockingException,你应该根据你的业务逻辑处理竞争条件(合并、重试、忽略)。这很简单,只需将以下标准添加到 乐观锁模型 的更新查询中即可

<?php
$query->where('id', $this->id)
    ->where('lock_version', $this->lock_version)
    ->update($changes);

如果资源在您的更新尝试之前已被更新,则上述操作将简单地更新 记录,这意味着模型在当前尝试之前已被更新,或已被删除。

我们为什么不使用 updated_at 来跟踪更改?

因为它们在两个并发更新期间可能保持不变。

贡献

这是一个开源软件包,我们鼓励你通过以下方式做出贡献

与代码一起工作

项目使用Docker Compose定义开发中使用的容器。尽管不一定要使用Docker,但它可以使处理多个PHP版本变得更加容易。

在将git仓库分支克隆到本地开发环境后,您可以执行以下docker compose命令

安装依赖

docker compose run --rm php composer install

执行测试

docker compose run --rm php ./vendor/bin/phpunit

执行带覆盖率报告的测试

docker compose run --rm --env=XDEBUG_MODE=coverage php php ./vendor/bin/phpunit --coverage-text

执行PHPStan分析

docker compose run --rm phpstan

许可协议

MIT许可协议(MIT)。有关更多信息,请参阅许可文件