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.