0

Cache tags — grouping and busting related cache entries

Intermediate5 min read·lv-20-003
interview

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 WithoutOverlapping middleware).
  • 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
<?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