mpyw/laravel-database-advisory-lock

Laravel 上 PostgreSQL/MySQL/MariaDB 的咨询锁功能

v4.3.0 2023-09-26 02:47 UTC

This package is auto-updated.

Last update: 2024-09-22 02:52:11 UTC


README

Laravel 上 PostgreSQL/MySQL/MariaDB 的咨询锁功能

要求

安装

composer require mpyw/laravel-database-advisory-lock:^4.3

基本用法

重要

默认实现由 ConnectionServiceProvider 提供,但是,包发现不可用。请注意,您必须自己将其注册到 config/app.php

<?php

return [

    /* ... */

    'providers' => [
        /* ... */

        Mpyw\LaravelDatabaseAdvisoryLock\ConnectionServiceProvider::class,

        /* ... */
    ],

];
<?php

use Illuminate\Support\Facades\DB;
use Illuminate\Database\ConnectionInterface;

// Session-Level Locking
$result = DB::advisoryLocker()
    ->forSession()
    ->withLocking('<key>', function (ConnectionInterface $conn) {
        // critical section here
        return ...;
    }); // no wait
$result = DB::advisoryLocker()
    ->forSession()
    ->withLocking('<key>', function (ConnectionInterface $conn) {
        // critical section here
        return ...;
    }, timeout: 5); // wait for 5 seconds or fail
$result = DB::advisoryLocker()
    ->forSession()
    ->withLocking('<key>', function (ConnectionInterface $conn) {
        // critical section here
        return ...;
    }, timeout: -1); // infinite wait (except MariaDB)

// Postgres only feature: Transaction-Level Locking (no wait)
$result = DB::transaction(function (ConnectionInterface $conn) {
    $conn->advisoryLocker()->forTransaction()->lockOrFail('<key>');
    // critical section here
    return ...;
});

高级用法

提示

您可以通过 AdvisoryLocks 特性自己扩展连接类。

<?php

namespace App\Providers;

use App\Database\PostgresConnection;
use Illuminate\Database\Connection;
use Illuminate\Support\ServiceProvider;

class DatabaseServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        Connection::resolverFor('pgsql', function (...$parameters) {
            return new PostgresConnection(...$parameters);
        });
    }
}
<?php

namespace App\Database;

use Illuminate\Database\PostgresConnection as BasePostgresConnection;
use Mpyw\LaravelDatabaseAdvisoryLock\AdvisoryLocks;

class PostgresConnection extends BasePostgresConnection
{
    use AdvisoryLocks;
}

实现细节

键哈希算法

-- Postgres: int8
hashtext('<key>')
-- MySQL/MariaDB: varchar(64)
CASE WHEN CHAR_LENGTH('<key>') > 64
THEN CONCAT(SUBSTR('<key>', 1, 24), SHA1('<key>'))
ELSE '<key>'
END
  • PostgreSQL 咨询锁定函数仅接受整数键。因此,驱动程序通过 hashtext() 函数将键字符串转换为 64 位整数。
    • 空字符串也可以用作键。
  • MySQL 咨询锁定函数接受字符串键,但其长度限制在 64 个字符以内。当键字符串超过 64 个字符限制时,驱动程序将取其前 24 个字符,并附加 40 个字符的 sha1() 哈希。
    • MariaDB 的限制实际上是 192 字节,而 MySQL 的限制是 64 个字符。然而,键哈希算法是等效的。
    • MariaDB 接受空字符串作为键,但实际上并不锁定任何内容。另一方面,MySQL 对于空字符串键会引发错误。
  • 无论使用哪种哈希算法,碰撞理论上都只会以极低的概率发生。

锁定方法

  • 会话级锁可以在任何地方获取。
    • 它们可以手动释放或通过析构函数自动释放。
    • 对于 PostgreSQL,自动释放锁的算法存在问题,但在版本 4.0.0 中已修复。有关详细信息,请参阅 #2
  • 事务级锁可以在事务内获取。
    • 您不需要也无法手动释放已获取的锁。

超时值

  • PostgreSQL 本身不支持等待有限的具体时间,但这可以通过循环临时函数来模拟。
  • MariaDB 不接受无限的超时值。可以使用非常大的数字代替。
  • MySQL/MariaDB 不支持浮点精度。

关于事务级别的注意事项

关键原则

在使用咨询锁时,始终避免嵌套事务,以确保遵守 S2PL (严格两阶段锁定) 原则。

推荐方法

当事务和咨询锁相关时,可以应用任何锁定方法。

注意

事务级锁
在事务嵌套级别 1 获取锁,然后依靠自动释放机制。

if (DB::transactionLevel() > 1) {
    throw new LogicException("Don't use nested transactions outside of this logic.");
}

DB::advisoryLocker()
    ->forTransaction()
    ->lockOrFail('<key>');
// critical section with transaction here

注意

会话级锁
在事务嵌套级别 0 获取锁,然后调用 DB::transaction()

if (DB::transactionLevel() > 0) {
    throw new LogicException("Don't use transactions outside of this logic.");
}

$result = DB::advisoryLocker()
    ->forSession()
    ->withLocking('<key>', fn (ConnectionInterface $conn) => $conn->transaction(function () {
        // critical section with transaction here
    }));

警告

当编写此类逻辑时,必须使用 DatabaseTruncation 而不是 RefreshDatabase

考虑

注意

事务级锁
不要在嵌套事务中获取事务级别的锁。它们不了解Laravel的嵌套事务模拟。

注意

会话级锁
当事务要提交的内容与咨询锁相关时,不要在事务中获取会话级别的锁。

如果我们在一个事务中释放会话级别的锁会发生什么?让我们通过时间线图表来验证,假设Postgres上的READ COMMITTED隔离级别。账户X由两个会话A和B同时操作。

会话A 会话B
BEGIN
…… BEGIN
pg_advisory_lock(X) ……
…… pg_advisory_lock(X)
获取用户X的余额
(余额:1000美元)
……
…… ……
如果余额允许,扣除800美元
(余额:1000美元 → 200美元)
……
…… ……
pg_advisory_unlock(X) ……
…… 获取用户X的余额
(余额:1000美元 ❗)
…… ……
…… 如果余额允许,扣除800美元
(余额:1000美元 → 200美元 ‼️)
COMMIT ……
…… pg_advisory_unlock(X)
获取用户X的余额
(余额:200美元)
……
COMMIT
……
获取用户X的余额
余额: -600美元 ⁉️⁉️⁉️)