Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Redis Lock Laravel Package

ginnerpeace/laravel-redis-lock

A Laravel package that adds Redis-backed locks for coordinating concurrent work across processes and servers. A simple API lets you acquire, release, and manage locks with TTLs and blocking waits, helping prevent race conditions in queued jobs, artisan commands, and scheduled tasks.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and confirm what you actually got. The package is small (15 stars, last release 2024-11-18), so before building on it, spend ten minutes reading the source — it is likely a thin wrapper around SET key value NX PX ttl plus a Lua release script.

composer require ginnerpeace/laravel-redis-lock

Then check three things:

  1. Auto-discovery — look for extra.laravel.providers in the package's composer.json. If absent, register the provider manually in bootstrap/providers.php (Laravel 11+) or config/app.php.
  2. Config — if the package ships a config file, publish it (php artisan vendor:publish --provider="Ginnerpeace\RedisLock\...") and confirm which Redis connection it uses. It should be a dedicated connection, not your cache connection (see Gotchas).
  3. API surface — open the facade/class and note the real method names. Examples below use acquire() / release() as illustrative names; adjust to the actual signature before copying.

Redis must be reachable and configured. Verify config/database.php has your redis connection and that REDIS_CLIENT (phpredis or predis) is installed:

php artisan tinker
>>> Redis::connection()->ping();

First use case — stop duplicate work. The highest-value first use is guarding a non-idempotent action that can be triggered twice: a webhook handler, a payment capture, or a scheduled command that must not overlap across servers.

use Illuminate\Support\Facades\Log;
// use Ginnerpeace\RedisLock\Facades\RedisLock; // confirm facade name

$lock = RedisLock::acquire('lock:invoice:'.$invoice->id, ttl: 30);

if (! $lock) {
    Log::info('invoice.capture.skipped', ['id' => $invoice->id]);
    return; // someone else owns it
}

try {
    $invoice->capture();
} finally {
    $lock->release(); // must be owner-checked; see Gotchas
}

Get this pattern working end-to-end on one endpoint before rolling it out. If it behaves correctly under a deliberate double-click, you understand the package's semantics well enough to use it elsewhere.

Important baseline: Laravel has shipped atomic locks since 5.8 — Cache::lock('invoice:'.$id, 30)->get() / ->block(5) — backed by Redis with an owner token and Lua-based release. Evaluate this package against that before adopting it broadly. Adopt it only if it offers something the built-in does not (simpler API, auto-refresh, a specific TTL/retry policy, or an opinionated key prefix).

Implementation Patterns

1. Skip-if-busy (non-blocking). Best for "only one run at a time" semantics: cron commands, cache warmers, sync jobs. Fail fast, log, exit 0 — do not treat a skipped run as an error.

if (! $lock = RedisLock::acquire($key, ttl: 300)) {
    $this->info('Already running elsewhere; skipping.');
    return self::SUCCESS;
}
try { $this->doWork(); } finally { $lock->release(); }

2. Block-with-timeout (serialize). Best for read-modify-write on a shared row (inventory decrement, sequence numbers). Wait briefly, then bail with a domain error rather than blocking a PHP-FPM worker indefinitely.

$lock = RedisLock::acquire("lock:stock:{$sku}", ttl: 10, wait: 3);
abort_if(! $lock, 409, 'Busy, retry.');
try { $stock->decrement('qty', $n); } finally { $lock->release(); }

3. Always try/finally. The single most common bug is an early return or thrown exception between acquire and release, leaving the resource locked until TTL expiry. Wrap every acquisition in try/finally, no exceptions.

4. Key design. Namespace by environment, tenant, and resource, and include a version if the payload shape changes:

lock:{env}:{tenant}:{resource}:{id}
lock:prod:acme:invoice:8842

Keep keys stable and greppable — you will be scanning for them in redis-cli during incidents.

5. Long-running work needs a heartbeat. A lock TTL is a lease, not a mutex. If work can exceed the TTL (large exports, external API calls with retries), either (a) size the TTL generously (2–3× p99 runtime), (b) refresh the lock periodically if the package exposes refresh/extend, or (c) restructure so the lock covers only a short critical section and the long work is queued outside it.

6. Queue jobs. Laravel's WithoutOverlapping middleware already solves per-job uniqueness and is usually the better tool for jobs. Reserve this package for cases where the critical section spans different job classes or a job + a controller (e.g., "only one of these three entry points may mutate this order").

7. Wrap it, don't scatter it. Introduce one small service so the rest of the app isn't coupled to a 15-star dependency:

final class LockManager
{
    public function run(string $key, int $ttl, Closure $fn, int $wait = 0): mixed
    {
        $lock = RedisLock::acquire($key, $ttl, $wait);
        if (! $lock) throw new LockUnavailable($key);
        try { return $fn(); } finally { $lock->release(); }
    }
}

This gives you one place to swap in Cache::lock(), add metrics, or fix a key prefix later.

8. Integration with HTTP requests. For user-triggered mutations (double submit, double click, retry storms), acquire a short-TTL lock keyed on {user}:{action}:{resource} and return 409/429 when it is held. Pair with an idempotency key column for true safety — the lock prevents concurrency, the idempotency key prevents duplication after the lock is released.

9. Testing. Run a real Redis in CI (or a redis service container) — lock semantics cannot be faked meaningfully. Use a separate DB index, and flushdb between tests. Write at least one test asserting contention: two acquisitions, second fails; then release, second succeeds.

Gotchas and Tips

  • Compare with Cache::lock() first. Laravel's built-in Redis lock is battle-tested, has an owner token, and releases via Lua. If this package is a thin wrapper, you may be adding a dependency for no gain. Check whether the package's release path is ownership-safe (GET-then-DEL is a race; a Lua compare-and-delete is correct).
  • TTL expiry is silent corruption. If work runs longer than the TTL, a second worker acquires the lock and both mutate state — no error is raised. This is the #1 failure mode. Keep critical sections short; size TTLs from measured p99; add a log line when work exceeds ~50% of TTL.
  • Release must be owner-checked. If release() deletes by key only, worker A can delete worker B's lock after A's TTL expired. Confirm the package compares a token. If it doesn't, wrap Cache::lock() instead.
  • Dedicated Redis connection. Do not share the lock connection with a cache configured with allkeys-lru — Redis can evict your lock under memory pressure and your mutex vanishes. Use noeviction for the lock DB, or at minimum a separate database index.
  • Blocking waits consume workers. A wait of 30s under contention ties up PHP-FPM processes and can cascade into a full-app outage. Prefer non-blocking + client retry, and keep waits ≤ a few seconds.
  • Not Redlock. A single-node lock is not safe across Redis failover (replication is async; a promoted replica may not have your SET NX). For anything financially material, design for idempotency at the DB layer rather than trusting the lock alone.
  • Config quirks to check: default TTL units (seconds vs milliseconds), default connection name, key prefix handling, and whether the TTL is enforced as a constructor arg or a config default. A millisecond/second mix-up produces locks that either vanish instantly or linger for hours.
  • Debugging. redis-cli -n <db> --scan --pattern 'lock:*' and TTL <key> are your first stop. MONITOR in staging shows acquisition/release in real time — use it once to confirm the exact commands and expiry flags the package issues.
  • Maintenance risk. 15 stars and a single maintainer means you should pin the version in composer.json ("^1.0", not "*"), keep the wrapper service described above, and be ready to migrate to Cache::lock(). Read the CHANGELOG before bumping; verify Laravel 11/12 compatibility explicitly against your app version.
  • Extension points. Prefer logging/metrics at your wrapper layer (acquisition failures, hold duration, release-after-expiry) over patching the package. If you need auto-refresh or Redlock semantics, reach for a more established library rather than extending this one.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core