mpyw / laravel-database-advisory-lock
Laravel 上 PostgreSQL/MySQL/MariaDB 的咨询锁功能
v4.3.0
2023-09-26 02:47 UTC
Requires
- php: ^8.0.2
- ext-pdo: *
- illuminate/contracts: ^9.0 || ^10.0 || ^11.0
- illuminate/database: ^9.0 || ^10.0 || ^11.0
- illuminate/events: ^9.0 || ^10.0 || ^11.0
- illuminate/support: ^9.0 || ^10.0 || ^11.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.9
- nunomaduro/larastan: >=1.0
- orchestra/testbench: *
- orchestra/testbench-core: >=7.0
- phpstan/extension-installer: >=1.1
- phpstan/phpstan: >=1.1
- phpunit/phpunit: >=9.5
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美元 ⁉️⁉️⁉️) |