| Install | |
|---|---|
composer require mpyw/laravel-database-advisory-lock |
|
| Latest Version: | v4.4.1 |
| PHP: | ^8.2 |
Advisory Locking Features of Postgres/MySQL/MariaDB on Laravel
| Package | Version | Mandatory |
|---|---|---|
| PHP | ^8.2 |
✅ |
| Laravel | ^11.0 || ^12.0 |
✅ |
| PHPStan | >=2.0 |
[!NOTE] Older versions have outdated dependency requirements. If you cannot prepare the latest environment, please refer to past releases.
| RDBMS | Version |
|---|---|
| Postgres | >=9.1.14 |
| MySQL | >=5.7.5 |
| MariaDB | >=10.0.15 |
composer require mpyw/laravel-database-advisory-lock:^4.4
[!IMPORTANT] The default implementation is provided by
ConnectionServiceProvider, however, package discovery is not available. Be careful that you MUST register it inconfig/app.phpby yourself.
<?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 ...;
});
[!TIP] You can extend Connection classes with
AdvisoryLockstrait by yourself.
<?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
hashtext() function.
sha1() hashes.
| Postgres | MySQL/MariaDB | |
|---|---|---|
| Session-Level Locking | ✅ | ✅ |
| Transaction-Level Locking | ✅ | ❌ |
| Postgres | MySQL | MariaDB | |
|---|---|---|---|
Timeout: 0 (default; immediate, no wait) |
✅ | ✅ | ✅ |
Timeout: positive-int |
✅ | ✅ | ✅ |
Timeout: negative-int (infinite wait) |
✅ | ✅ | ❌ |
Timeout: float |
✅ | ❌ | ❌ |
When transactions and advisory locks are related, either locking approach can be applied.
[!TIP] For Postgres, always prefer Transaction-Level Locking.
[!NOTE] Transaction-Level Locks:
Ensure the current context is inside the transaction, then rely on automatic release mechanisms.if (DB::transactionLevel() < 1) { throw new LogicException("Unexpectedly transaction is not active."); } DB::advisoryLocker() ->forTransaction() ->lockOrFail('<key>'); // critical section with transaction here
[!NOTE] Session-Level Locks:
Ensure the current context is outside the transaction, then proceed to callDB::transaction()call.if (DB::transactionLevel() > 0) { throw new LogicException("Unexpectedly transaction is already active."); } $result = DB::advisoryLocker() ->forSession() ->withLocking('<key>', fn (ConnectionInterface $conn) => $conn->transaction(function () { // critical section with transaction here }));
[!WARNING] When writing logic like this,
DatabaseTruncationmust be used instead ofRefreshDatabase.
[!CAUTION] Session-Level Locks:
Don't take session-level locks in the transactions when the content to be committed by the transaction is related to the advisory locks.What would happen if we released a session-level lock within a transaction? Let's verify this with a timeline chart, assuming a
READ COMMITTEDisolation level on Postgres. The bank account X is operated from two sessions A and B concurrently.
Session A Session B BEGIN︙ BEGINpg_advisory_lock(X)︙ ︙ pg_advisory_lock(X)Fetch balance of User X
(Balance: 1000 USD)︙ ︙ ︙ Deduct 800 USD if balance permits
(Balance: 1000 USD → 200 USD)︙ ︙ ︙ pg_advisory_unlock(X)︙ ︙ Fetch balance of User X
(Balance: 1000 USD :heavy_exclamation_mark:)︙ ︙ ︙ Deduct 800 USD if balance permits
(Balance: 1000 USD → 200 USD :bangbang:)COMMIT︙ ︙ pg_advisory_unlock(X)Fetch balance of User X
(Balance: 200 USD)︙ COMMIT︙ Fetch balance of User X
(Balance: -600 USD :interrobang::interrobang::interrobang:)