Redis Caching Patterns

Redis Mastery

Lesson 4 35 min Free Preview

Redis Caching Patterns

Implement effective caching strategies: Cache-Aside, Write-Through, and more.

Redis Caching Patterns

Effective caching can dramatically improve application performance. This lesson covers common caching patterns used in production applications.

�� Further Reading

For more on caching patterns, see Redis Client-side Caching

1. Cache-Aside (Lazy Loading)

The most common pattern. Application checks cache first, loads from database on miss, then populates cache.

<?php
function getUser(int $id): ?array
{
    global $redis, $db;
    
    $cacheKey = "user:{$id}";
    
    // 1. Try cache first
    $cached = $redis->get($cacheKey);
    if ($cached !== null) {
        return json_decode($cached, true);
    }
    
    // 2. Cache miss - load from database
    $user = $db->select('users', '*', ['id' => $id]);
    if (empty($user)) {
        return null;
    }
    
    // 3. Populate cache with TTL
    $redis->setex($cacheKey, 3600, json_encode($user[0]));
    
    return $user[0];
}

Pros: Only caches data that is actually requested. Cache misses are automatically filled.

Cons: First request always hits database. Can have stale data if TTL is too long.

2. Write-Through

Data is written to cache and database at the same time.

<?php
function updateUser(int $id, array $data): bool
{
    global $redis, $db;
    
    // 1. Update database
    $result = $db->update('users', $data, ['id' => $id]);
    
    if ($result->rowCount() > 0) {
        // 2. Update cache
        $user = $db->select('users', '*', ['id' => $id])[0];
        $redis->setex("user:{$id}", 3600, json_encode($user));
        return true;
    }
    
    return false;
}

Pros: Cache is always consistent with database.

Cons: Write latency increases. May cache data that is never read.

3. Write-Behind (Write-Back)

Write to cache immediately, then asynchronously write to database.

<?php
function updateUserAsync(int $id, array $data): void
{
    global $redis;
    
    // 1. Update cache immediately
    $redis->hMSet("user:{$id}", $data);
    
    // 2. Queue database write for later
    $redis->lPush('queue:db_writes', json_encode([
        'table' => 'users',
        'id' => $id,
        'data' => $data,
        'timestamp' => time()
    ]));
}

// Background worker processes the queue
function processDbWriteQueue(): void
{
    global $redis, $db;
    
    while ($job = $redis->rPop('queue:db_writes')) {
        $task = json_decode($job, true);
        $db->update($task['table'], $task['data'], ['id' => $task['id']]);
    }
}

Pros: Very fast writes. Good for high write throughput.

Cons: Risk of data loss if Redis crashes. More complex to implement.

4. Cache Invalidation

When data changes, you need to invalidate (delete) the cached version.

<?php
// Simple invalidation
function deleteUser(int $id): void
{
    global $redis, $db;
    
    $db->delete('users', ['id' => $id]);
    $redis->del("user:{$id}");
}

// Tag-based invalidation for related data
class TaggedCache
{
    private $redis;
    
    public function set(string $key, mixed $value, array $tags, int $ttl = 3600): void
    {
        $this->redis->setex($key, $ttl, serialize($value));
        
        // Track which keys belong to each tag
        foreach ($tags as $tag) {
            $this->redis->sAdd("tag:{$tag}", $key);
        }
    }
    
    public function invalidateTag(string $tag): int
    {
        $keys = $this->redis->sMembers("tag:{$tag}");
        $count = 0;
        
        if (!empty($keys)) {
            $count = $this->redis->del(...$keys);
        }
        
        $this->redis->del("tag:{$tag}");
        return $count;
    }
}

// Usage
$cache = new TaggedCache($redis);
$cache->set("user:1:profile", $profile, ["user:1"]);
$cache->set("user:1:settings", $settings, ["user:1"]);

// When user data changes, invalidate all related cache
$cache->invalidateTag("user:1");

5. Cache Stampede Prevention

When a popular cache key expires, many requests may hit the database simultaneously. Use locking to prevent this.

<?php
function getWithLock(string $key, callable $loader, int $ttl = 3600): mixed
{
    global $redis;
    
    // Try cache
    $value = $redis->get($key);
    if ($value !== null) {
        return unserialize($value);
    }
    
    // Try to acquire lock
    $lockKey = "lock:{$key}";
    $acquired = $redis->set($lockKey, '1', 'NX', 'EX', 10);
    
    if ($acquired) {
        // We got the lock - load data
        try {
            $value = $loader();
            $redis->setex($key, $ttl, serialize($value));
            return $value;
        } finally {
            $redis->del($lockKey);
        }
    }
    
    // Another process is loading - wait and retry
    usleep(100000); // 100ms
    return getWithLock($key, $loader, $ttl);
}

TTL Best Practices

Data Type Suggested TTL Reason
Session data 15-60 minutes Security, user activity
User profile 1-24 hours Rarely changes
API response 5-60 minutes Depends on freshness needs
Static content 1-7 days Rarely changes

🎓 Free Preview Complete

You have completed the free Redis lessons! Premium lessons cover Pub/Sub, transactions, Lua scripting, clustering, and more.