PHP cookbook · Beginner

Cache expensive queries with Redis in PHP

Speed up a slow product listing query with Redis while keeping data reasonably fresh.

Approach

Implement cache-aside: read from Redis; on a miss, compute from the database and store with a TTL (plus jitter). Invalidate keys when data changes, and use a short lock to prevent a stampede when a hot key expires.

Solution

<?php
declare(strict_types=1);

final class Cache
{
    public function __construct(private \Redis $redis) {}

    public function remember(string $key, int $ttl, callable $compute): mixed
    {
        $cached = $this->redis->get($key);
        if ($cached !== false) {
            return unserialize($cached, ['allowed_classes' => false]);
        }
        // Only one process recomputes; others briefly wait and retry
        $lock = $this->redis->set("lock:{$key}", '1', ['nx', 'ex' => 10]);
        if (!$lock) {
            usleep(100_000);
            return $this->remember($key, $ttl, $compute);
        }
        try {
            $value = $compute();
            $this->redis->setex($key, $ttl + random_int(0, (int) ($ttl * 0.1)), serialize($value));
            return $value;
        } finally {
            $this->redis->del("lock:{$key}");
        }
    }
}

$products = $cache->remember('products:featured:v2', 300, fn () => $repo->featured(limit: 20));
// After an admin edits featured products:
$redis->del('products:featured:v2');

Pitfalls

  • Caching per-user data under shared keys.
  • unserialize() of untrusted data with objects allowed — disable classes or use JSON.
  • Infinite waiting on locks if the computing process crashes (locks need TTLs).

Variations

  • Use APCu for per-server caches of config-like data.
  • Laravel: Cache::remember() / Cache::flexible() for stale-while-revalidate.

Further reading: Redis patterns

Reference: https://redis.io/docs/latest/develop/use/patterns/

More PHP recipes