Redis has become an essential tool for building high-performance applications. Let's explore advanced caching patterns and strategies to optimize your application's performance.
Understanding Redis Caching Patterns 🚀
Key caching strategies include:
- Cache-Aside
- Write-Through
- Write-Behind
- Refresh-Ahead
- Cache Invalidation
- Data Structures
Basic Cache Implementation
Implement cache-aside pattern:
class CacheManager {
constructor() {
this.redis = new Redis({
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT,
retryStrategy: times => Math.min(times * 50, 2000)
});
}
async get(key) {
const cached = await this.redis.get(key);
if (cached) {
return JSON.parse(cached);
}
const data = await this.fetchFromDatabase(key);
await this.set(key, data);
return data;
}
async set(key, value, ttl = 3600) {
await this.redis.set(
key,
JSON.stringify(value),
'EX',
ttl
);
}
}
Advanced Caching Strategies
Write-Through Cache
Implement write-through caching:
class WriteThroughCache {
constructor(redis, database) {
this.redis = redis;
this.database = database;
}
async set(key, value) {
try {
await Promise.all([
this.database.save(key, value),
this.redis.set(key, JSON.stringify(value))
]);
return true;
} catch (error) {
await this.invalidate(key);
throw error;
}
}
async invalidate(key) {
await this.redis.del(key);
}
}
Write-Behind Cache
Implement write-behind caching:
class WriteBehindCache {
constructor(options = {}) {
this.batchSize = options.batchSize || 100;
this.flushInterval = options.flushInterval || 5000;
this.writeQueue = new Map();
setInterval(
() => this.flush(),
this.flushInterval
);
}
async set(key, value) {
this.writeQueue.set(key, value);
if (this.writeQueue.size >= this.batchSize) {
await this.flush();
}
return true;
}
async flush() {
if (this.writeQueue.size === 0) return;
const batch = Array.from(this.writeQueue.entries());
this.writeQueue.clear();
try {
await this.database.batchSave(batch);
} catch (error) {
// Re-queue failed writes
batch.forEach(([key, value]) => {
this.writeQueue.set(key, value);
});
throw error;
}
}
}
Cache Invalidation Strategies
Time-Based Invalidation
Implement TTL-based invalidation:
class TimedCache {
constructor(redis) {
this.redis = redis;
}
async set(key, value, ttl) {
const multi = this.redis.multi();
multi.set(key, JSON.stringify(value));
multi.expire(key, ttl);
return multi.exec();
}
async setWithSlidingExpiration(key, value, ttl) {
const multi = this.redis.multi();
multi.set(key, JSON.stringify(value));
multi.expire(key, ttl);
const result = await multi.exec();
// Update expiration on read
this.redis.on('get', async (key) => {
await this.redis.expire(key, ttl);
});
return result;
}
}
Pattern-Based Invalidation
Implement pattern invalidation:
class PatternCache {
constructor(redis) {
this.redis = redis;
}
async invalidatePattern(pattern) {
const keys = await this.redis.keys(pattern);
if (keys.length > 0) {
return this.redis.del(keys);
}
return 0;
}
async setWithTags(key, value, tags) {
const multi = this.redis.multi();
multi.set(key, JSON.stringify(value));
tags.forEach(tag => {
multi.sadd(`tag:${tag}`, key);
});
return multi.exec();
}
async invalidateByTag(tag) {
const keys = await this.redis.smembers(`tag:${tag}`);
if (keys.length > 0) {
const multi = this.redis.multi();
multi.del(keys);
multi.del(`tag:${tag}`);
return multi.exec();
}
return 0;
}
}
Cache Warming Strategies
Implement cache warming:
class CacheWarmer {
constructor(redis, database) {
this.redis = redis;
this.database = database;
}
async warmCache(patterns) {
const data = await this.database.fetchWarmingData(
patterns
);
const multi = this.redis.multi();
data.forEach(item => {
multi.set(
this.generateKey(item),
JSON.stringify(item)
);
});
return multi.exec();
}
async warmCacheInBackground(patterns) {
setImmediate(async () => {
try {
await this.warmCache(patterns);
} catch (error) {
console.error('Cache warming failed:', error);
}
});
}
}
Performance Optimization
Pipeline Implementation
Optimize multiple operations:
class RedisPipeline {
constructor(redis) {
this.redis = redis;
}
async batchGet(keys) {
const pipeline = this.redis.pipeline();
keys.forEach(key => {
pipeline.get(key);
});
const results = await pipeline.exec();
return results.map(([err, result]) => {
if (err) throw err;
return JSON.parse(result);
});
}
async batchSet(items) {
const pipeline = this.redis.pipeline();
items.forEach(([key, value]) => {
pipeline.set(key, JSON.stringify(value));
});
return pipeline.exec();
}
}
Memory Optimization
Implement memory-efficient caching:
class MemoryOptimizedCache {
constructor(redis, options = {}) {
this.redis = redis;
this.maxMemory = options.maxMemory || '2gb';
this.evictionPolicy = options.evictionPolicy || 'allkeys-lru';
this.configureMemory();
}
async configureMemory() {
await this.redis.config('SET', 'maxmemory', this.maxMemory);
await this.redis.config(
'SET',
'maxmemory-policy',
this.evictionPolicy
);
}
async monitorMemory() {
const info = await this.redis.info('memory');
return this.parseMemoryInfo(info);
}
}
Best Practices
- Cache Design
- Use appropriate data structures
- Implement proper TTLs
- Plan invalidation strategy
- Monitor memory usage
- Performance
- Use pipelining
- Implement batching
- Optimize key design
- Monitor hit rates
- Reliability
- Implement error handling
- Use proper serialization
- Monitor cache health
- Plan failover
- Development
- Document patterns
- Test thoroughly
- Monitor metrics
- Implement logging
Conclusion
Redis caching patterns are essential for building high-performance applications. Remember to:
- Choose appropriate patterns
- Implement proper invalidation
- Optimize performance
- Monitor usage
- Handle errors
- Test thoroughly
As your application scales, these patterns will help maintain performance and reliability.