Cache tags — grouping and busting related cache entries
Concept
Atomic locks provide distributed mutual exclusion — only one process can hold the lock at a time, across all servers. Built on Redis SETNX or Memcached add, which are atomic operations at the database level.
Cache::lock(string $name, int $ttl = 0): Creates a lock object. $ttl is the maximum seconds the lock is held (safety valve against crashes).
$lock->get(): Try to acquire immediately. Returns bool. Non-blocking.
$lock->get(callable $callback): Acquire and execute the callback atomically. Releases the lock after the callback (even if it throws). Returns false if lock unavailable.
$lock->block(int $seconds, callable $callback = null): Wait up to $seconds for the lock. Throws LockTimeoutException if waiting too long without success.
$lock->release(): Manually release the lock.
$lock->forceRelease(): Release even if owned by another owner. Use with care.
Owner: Locks track who acquired them. A process can only release locks it owns (unless using forceRelease). If a lock expires and another process acquires it, releasing via the original lock object won't affect the new owner.
Use cases:
- Prevent duplicate job processing (see also
WithoutOverlappingmiddleware). - Cache stampede prevention — only one process recomputes a cache value at a time.
- Distributed cron — only one server runs a scheduled command at a time.
Code Example
<?php
use Illuminate\Support\Facades\Cache;
// Non-blocking lock acquisition
$lock = Cache::lock('process-payments', 60); // 60s TTL
if ($lock->get()) {
try {
processPayments();
} finally {
$lock->release();
}
}
// Callback form — auto-releases on success or exception
$result = Cache::lock('generate-report', 120)->get(function() {
return generateExpensiveReport();
});
if ($result === false) {
// Another process is generating — tell user to wait
}
// Blocking — wait up to 5 seconds for lock
try {
Cache::lock('send-newsletter', 300)->block(5, function() {
sendNewsletter();
});
} catch (\Illuminate\Contracts\Cache\LockTimeoutException $e) {
// Couldn't acquire lock in 5 seconds
return response()->json(['error' => 'System is busy, try again'], 503);
}
// Cache stampede prevention — only one process warms the cache
function getCachedData(string $key, callable $callback): mixed
{
$value = Cache::get($key);
if ($value !== null) return $value;
// Use a lock to prevent multiple simultaneous cache misses computing the same value
$lock = Cache::lock("compute:$key", 30);
if ($lock->get()) {
try {
$value = $callback();
Cache::put($key, $value, 3600);
} finally {
$lock->release();
}
return $value;
}
// Another process is computing — wait briefly and return whatever is now cached
usleep(100000); // wait 100ms
return Cache::get($key, $callback()); // last resort: compute without cache
}
// Distributed cron — only one server runs the job
\Illuminate\Support\Facades\Schedule::command('report:generate')
->hourly()
->withoutOverlapping() // uses cache lock internally
->onOneServer(); // also ensures single server execution