วันที่เซิร์ฟเวอร์ล่มเพราะ Cache Miss
วันนั้นเป็นวันศุกร์ เวลา 14:30 น. ทั้งทีมกำลังจะไปกินข้าวเที่ยง แล้วมี alert เข้ามา:
”🚨 HIGH RESPONSE TIME: API endpoints responding > 5 seconds”
หน้าจอ monitoring เต็มไปด้วยสีแดง CPU usage พุ่งไป 100% database connections เต็ม response time จาก 200ms กลายเป็น 8 วินาที! 😱
สาเหตุ: Cache ของ popular API endpoint หมดอายุพร้อมกัน (TTL 1 hour) ทำให้ request หลายพันตัวพุ่งเข้า database ในเวลาเดียวกัน = Cache Stampede!
// Code ที่ทำให้เกิด Cache Stampede
app.get('/api/products/popular', async (req, res) => {
const cacheKey = 'popular_products';
// ❌ Simple cache pattern ที่อันตราย
let products = await redis.get(cacheKey);
if (!products) {
console.log('Cache miss - querying database...');
// Query ที่ช้ามาก (complex aggregation)
products = await db.query(`
SELECT p.*, AVG(r.rating), COUNT(o.id) as sales
FROM products p
LEFT JOIN reviews r ON p.id = r.product_id
LEFT JOIN orders o ON p.id = o.product_id
WHERE p.status = 'active'
GROUP BY p.id
ORDER BY sales DESC
LIMIT 50
`); // ใช้เวลา 3-5 วินาที
await redis.setex(cacheKey, 3600, JSON.stringify(products));
} else {
products = JSON.parse(products);
}
res.json(products);
});
ปัญหา: พอ cache หมดอายุ request 1000 ตัว พุ่งเข้ามาพร้อมกัน ทำให้ database ไม่ไหว! 💥
การเรียนรู้ Redis แบบ Trial by Fire
หลังจากเหตุการณ์นั้น ได้ศึกษา Redis จริงจัง เพื่อไม่ให้เกิดเหตุการณ์แบบนี้อีก
เริ่มต้นกับ Redis Basics
// redis-client.js - Setup พื้นฐาน
const Redis = require('ioredis');
const redis = new Redis({
host: 'localhost',
port: 6379,
password: 'your-password',
db: 0,
retryDelayOnFailover: 100,
maxRetriesPerRequest: 3,
lazyConnect: true,
});
redis.on('connect', () => {
console.log('Redis connected');
});
redis.on('error', (err) => {
console.error('Redis error:', err);
});
// Basic operations
await redis.set('key', 'value');
await redis.setex('key', 60, 'value'); // TTL 60 seconds
await redis.get('key');
await redis.del('key');
Cache Stampede Solution
1. Cache-Aside with Lock Pattern
// cache-service.js - แก้ปัญหา Cache Stampede
class CacheService {
constructor(redis) {
this.redis = redis;
this.locks = new Map(); // In-memory locks สำหรับ process เดียวกัน
}
async getOrSet(key, fetcher, ttl = 3600, options = {}) {
const {
lockTimeout = 30000, // 30 seconds
staleWhileRevalidate = 300, // 5 minutes
} = options;
try {
// 1. พยายาม get จาก cache ก่อน
const cached = await this.redis.get(key);
if (cached) {
return JSON.parse(cached);
}
// 2. ถ้าไม่มี ลองได้ lock
const lockKey = `lock:${key}`;
const lockValue = Date.now() + Math.random();
const lockAcquired = await this.redis.set(
lockKey,
lockValue,
'PX', lockTimeout, // milliseconds
'NX' // only if not exists
);
if (lockAcquired === 'OK') {
try {
// 3. Double check cache (อาจจะมีคนอื่นเพิ่งเขียน)
const doubleCheck = await this.redis.get(key);
if (doubleCheck) {
return JSON.parse(doubleCheck);
}
// 4. Fetch ข้อมูลจริง
console.log(`Cache miss for ${key} - fetching...`);
const data = await fetcher();
// 5. Store ใน cache
await this.redis.setex(key, ttl, JSON.stringify(data));
return data;
} finally {
// 6. Release lock
await this.redis.eval(`
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
`, 1, lockKey, lockValue);
}
} else {
// 7. ถ้าไม่ได้ lock รอแล้วลองใหม่
console.log(`Waiting for cache refresh: ${key}`);
await this.sleep(100 + Math.random() * 200); // Jitter
return this.getOrSet(key, fetcher, ttl, options);
}
} catch (error) {
console.error('Cache error:', error);
// Fallback ไป fetch โดยตรง
return await fetcher();
}
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// Usage
const cacheService = new CacheService(redis);
app.get('/api/products/popular', async (req, res) => {
try {
const products = await cacheService.getOrSet(
'popular_products',
async () => {
return await db.query(/* complex query */);
},
3600 // 1 hour
);
res.json(products);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch products' });
}
});
2. Stale-While-Revalidate Pattern
// stale-while-revalidate.js
class StaleWhileRevalidateCache {
constructor(redis) {
this.redis = redis;
}
async get(key, fetcher, ttl = 3600, staleTtl = 7200) {
const data = await this.redis.hmget(key, 'value', 'expires', 'refreshing');
const [value, expires, refreshing] = data;
const now = Date.now();
const expiresAt = parseInt(expires) || 0;
// Case 1: Cache hit และยังไม่หมดอายุ
if (value && now < expiresAt) {
return JSON.parse(value);
}
// Case 2: Cache stale แต่ยังใช้ได้
if (value && refreshing !== 'true') {
// Return stale data ทันที
const result = JSON.parse(value);
// Background refresh (fire and forget)
this.refreshInBackground(key, fetcher, ttl, staleTtl);
return result;
}
// Case 3: ไม่มี cache หรือ refreshing แล้ว
return await this.fetchAndCache(key, fetcher, ttl, staleTtl);
}
async refreshInBackground(key, fetcher, ttl, staleTtl) {
try {
// Set refreshing flag
await this.redis.hset(key, 'refreshing', 'true');
const fresh = await fetcher();
const now = Date.now();
await this.redis.hmset(key,
'value', JSON.stringify(fresh),
'expires', now + (ttl * 1000),
'refreshing', 'false'
);
await this.redis.expire(key, staleTtl);
console.log(`Background refresh completed for ${key}`);
} catch (error) {
console.error('Background refresh failed:', error);
await this.redis.hset(key, 'refreshing', 'false');
}
}
async fetchAndCache(key, fetcher, ttl, staleTtl) {
const fresh = await fetcher();
const now = Date.now();
await this.redis.hmset(key,
'value', JSON.stringify(fresh),
'expires', now + (ttl * 1000),
'refreshing', 'false'
);
await this.redis.expire(key, staleTtl);
return fresh;
}
}
// Usage
const swrCache = new StaleWhileRevalidateCache(redis);
app.get('/api/products/popular', async (req, res) => {
const products = await swrCache.get(
'popular_products',
() => fetchPopularProducts(),
3600, // fresh for 1 hour
7200 // stale for 2 hours total
);
res.json(products);
});
Multi-Layer Caching Strategy
// multi-layer-cache.js
class MultiLayerCache {
constructor(options = {}) {
this.memory = new Map();
this.redis = options.redis;
this.memoryTtl = options.memoryTtl || 300; // 5 minutes
this.redisTtl = options.redisTtl || 3600; // 1 hour
this.maxMemorySize = options.maxMemorySize || 1000;
}
async get(key, fetcher) {
// Layer 1: Memory cache (fastest)
const memoryItem = this.memory.get(key);
if (memoryItem && Date.now() < memoryItem.expires) {
console.log(`Memory cache HIT: ${key}`);
return memoryItem.data;
}
// Layer 2: Redis cache
const redisData = await this.redis.get(key);
if (redisData) {
console.log(`Redis cache HIT: ${key}`);
const parsed = JSON.parse(redisData);
// Store ใน memory สำหรับครั้งถัดไป
this.setMemory(key, parsed);
return parsed;
}
// Layer 3: Original source
console.log(`Cache MISS: ${key} - fetching from source`);
const fresh = await fetcher();
// Store ทุก layers
await this.set(key, fresh);
return fresh;
}
async set(key, data) {
// Store ใน Redis
await this.redis.setex(key, this.redisTtl, JSON.stringify(data));
// Store ใน Memory
this.setMemory(key, data);
}
setMemory(key, data) {
// Evict oldest if memory full
if (this.memory.size >= this.maxMemorySize) {
const firstKey = this.memory.keys().next().value;
this.memory.delete(firstKey);
}
this.memory.set(key, {
data,
expires: Date.now() + (this.memoryTtl * 1000)
});
}
async invalidate(key) {
this.memory.delete(key);
await this.redis.del(key);
}
// Batch invalidation
async invalidatePattern(pattern) {
// Clear memory cache
for (const key of this.memory.keys()) {
if (key.includes(pattern)) {
this.memory.delete(key);
}
}
// Clear Redis cache
const keys = await this.redis.keys(`*${pattern}*`);
if (keys.length > 0) {
await this.redis.del(...keys);
}
}
}
// Usage
const cache = new MultiLayerCache({
redis,
memoryTtl: 300,
redisTtl: 3600,
maxMemorySize: 500
});
// Product cache with dependencies
app.get('/api/product/:id', async (req, res) => {
const productId = req.params.id;
const product = await cache.get(`product:${productId}`, async () => {
return await db.products.findById(productId);
});
res.json(product);
});
// Invalidate เมื่อ product ถูก update
app.put('/api/product/:id', async (req, res) => {
const productId = req.params.id;
await db.products.update(productId, req.body);
// Invalidate related caches
await cache.invalidate(`product:${productId}`);
await cache.invalidatePattern('popular_products');
await cache.invalidatePattern(`category:${req.body.categoryId}`);
res.json({ success: true });
});
Advanced Redis Patterns
1. Cache Warming Strategy
// cache-warmer.js
class CacheWarmer {
constructor(redis, cacheService) {
this.redis = redis;
this.cache = cacheService;
this.warmingQueue = [];
this.isWarming = false;
}
async warmCriticalData() {
const criticalKeys = [
'popular_products',
'featured_categories',
'top_sellers',
'trending_items'
];
console.log('Starting cache warming...');
for (const key of criticalKeys) {
this.warmingQueue.push({
key,
priority: 1,
fetcher: this.getFetcher(key)
});
}
this.processWarmingQueue();
}
getFetcher(key) {
const fetchers = {
'popular_products': () => fetchPopularProducts(),
'featured_categories': () => fetchFeaturedCategories(),
'top_sellers': () => fetchTopSellers(),
'trending_items': () => fetchTrendingItems()
};
return fetchers[key];
}
async processWarmingQueue() {
if (this.isWarming || this.warmingQueue.length === 0) return;
this.isWarming = true;
try {
// Process 3 items concurrently
const batch = this.warmingQueue.splice(0, 3);
await Promise.all(batch.map(async (item) => {
try {
console.log(`Warming cache: ${item.key}`);
await this.cache.getOrSet(item.key, item.fetcher, 3600);
} catch (error) {
console.error(`Failed to warm ${item.key}:`, error);
}
}));
// Continue with remaining items
if (this.warmingQueue.length > 0) {
setTimeout(() => {
this.isWarming = false;
this.processWarmingQueue();
}, 1000); // 1 second delay between batches
} else {
console.log('Cache warming completed');
this.isWarming = false;
}
} catch (error) {
console.error('Cache warming error:', error);
this.isWarming = false;
}
}
// Scheduled warming (every hour)
startScheduledWarming() {
setInterval(() => {
this.warmCriticalData();
}, 60 * 60 * 1000);
}
}
// Usage
const warmer = new CacheWarmer(redis, cacheService);
// Warm on startup
warmer.warmCriticalData();
// Schedule regular warming
warmer.startScheduledWarming();
2. Distributed Cache Invalidation
// cache-invalidation.js
class DistributedCacheInvalidation {
constructor(redis) {
this.redis = redis;
this.subscriber = redis.duplicate();
this.localCache = new Map();
this.setupSubscription();
}
setupSubscription() {
this.subscriber.subscribe('cache:invalidate');
this.subscriber.on('message', (channel, message) => {
if (channel === 'cache:invalidate') {
try {
const { pattern, keys } = JSON.parse(message);
if (pattern) {
this.invalidateLocalPattern(pattern);
}
if (keys && Array.isArray(keys)) {
keys.forEach(key => this.localCache.delete(key));
}
console.log('Cache invalidated via pub/sub:', { pattern, keys });
} catch (error) {
console.error('Invalid cache invalidation message:', error);
}
}
});
}
async invalidate(keys) {
if (!Array.isArray(keys)) keys = [keys];
// Invalidate local cache
keys.forEach(key => this.localCache.delete(key));
// Invalidate Redis cache
if (keys.length > 0) {
await this.redis.del(...keys);
}
// Notify other instances
await this.redis.publish('cache:invalidate', JSON.stringify({ keys }));
}
async invalidatePattern(pattern) {
// Invalidate local cache
this.invalidateLocalPattern(pattern);
// Invalidate Redis cache
const keys = await this.redis.keys(`*${pattern}*`);
if (keys.length > 0) {
await this.redis.del(...keys);
}
// Notify other instances
await this.redis.publish('cache:invalidate', JSON.stringify({ pattern }));
}
invalidateLocalPattern(pattern) {
for (const key of this.localCache.keys()) {
if (key.includes(pattern)) {
this.localCache.delete(key);
}
}
}
}
// Usage
const invalidation = new DistributedCacheInvalidation(redis);
// Invalidate เมื่อมีการ update
app.put('/api/product/:id', async (req, res) => {
const productId = req.params.id;
await db.products.update(productId, req.body);
// Invalidate across all instances
await invalidation.invalidate([
`product:${productId}`,
'popular_products'
]);
res.json({ success: true });
});
3. Circuit Breaker for Cache
// cache-circuit-breaker.js
class CacheCircuitBreaker {
constructor(options = {}) {
this.failureThreshold = options.failureThreshold || 5;
this.timeout = options.timeout || 60000; // 1 minute
this.monitoringPeriod = options.monitoringPeriod || 10000; // 10 seconds
this.failures = 0;
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
this.nextAttempt = Date.now();
this.successCount = 0;
}
async execute(operation) {
if (this.state === 'OPEN') {
if (Date.now() < this.nextAttempt) {
throw new Error('Circuit breaker is OPEN');
}
this.state = 'HALF_OPEN';
this.successCount = 0;
}
try {
const result = await operation();
if (this.state === 'HALF_OPEN') {
this.successCount++;
if (this.successCount >= 3) {
this.state = 'CLOSED';
this.failures = 0;
}
} else {
this.failures = 0;
}
return result;
} catch (error) {
this.failures++;
if (this.failures >= this.failureThreshold) {
this.state = 'OPEN';
this.nextAttempt = Date.now() + this.timeout;
}
throw error;
}
}
}
// Usage with Redis operations
class ResilientCacheService {
constructor(redis) {
this.redis = redis;
this.circuitBreaker = new CacheCircuitBreaker({
failureThreshold: 5,
timeout: 30000
});
}
async get(key) {
try {
return await this.circuitBreaker.execute(async () => {
return await this.redis.get(key);
});
} catch (error) {
console.warn(`Cache GET failed for ${key}:`, error.message);
return null; // Graceful degradation
}
}
async set(key, value, ttl) {
try {
return await this.circuitBreaker.execute(async () => {
return await this.redis.setex(key, ttl, value);
});
} catch (error) {
console.warn(`Cache SET failed for ${key}:`, error.message);
// Continue without caching
}
}
}
Redis Cluster Configuration
1. Production Setup
// redis-cluster.js
const Redis = require('ioredis');
const cluster = new Redis.Cluster([
{
host: 'redis-node-1.example.com',
port: 7000,
},
{
host: 'redis-node-2.example.com',
port: 7000,
},
{
host: 'redis-node-3.example.com',
port: 7000,
}
], {
redisOptions: {
password: process.env.REDIS_PASSWORD,
connectTimeout: 10000,
lazyConnect: true,
maxRetriesPerRequest: 3,
},
enableOfflineQueue: false,
retryDelayOnFailover: 100,
enableReadyCheck: true,
maxRedirections: 16,
scaleReads: 'slave', // Read from slaves
});
cluster.on('error', (err) => {
console.error('Redis Cluster Error:', err);
});
cluster.on('connect', () => {
console.log('Redis Cluster Connected');
});
cluster.on('ready', () => {
console.log('Redis Cluster Ready');
});
export default cluster;
2. Monitoring และ Health Checks
// redis-monitor.js
class RedisMonitor {
constructor(redis) {
this.redis = redis;
this.stats = {
hits: 0,
misses: 0,
errors: 0
};
this.startMonitoring();
}
startMonitoring() {
setInterval(async () => {
try {
const info = await this.redis.info('stats');
const memory = await this.redis.info('memory');
console.log('Redis Stats:', {
keyspace_hits: this.extractValue(info, 'keyspace_hits'),
keyspace_misses: this.extractValue(info, 'keyspace_misses'),
used_memory: this.extractValue(memory, 'used_memory_human'),
connected_clients: this.extractValue(info, 'connected_clients'),
ops_per_sec: this.extractValue(info, 'instantaneous_ops_per_sec')
});
} catch (error) {
console.error('Redis monitoring error:', error);
}
}, 30000); // Every 30 seconds
}
extractValue(info, key) {
const match = info.match(new RegExp(`${key}:(.+)`));
return match ? match[1].trim() : 'N/A';
}
async healthCheck() {
try {
const start = Date.now();
await this.redis.ping();
const latency = Date.now() - start;
return {
status: 'healthy',
latency,
stats: this.stats
};
} catch (error) {
return {
status: 'unhealthy',
error: error.message,
stats: this.stats
};
}
}
recordHit() {
this.stats.hits++;
}
recordMiss() {
this.stats.misses++;
}
recordError() {
this.stats.errors++;
}
}
// Usage
const monitor = new RedisMonitor(redis);
// Health check endpoint
app.get('/health/redis', async (req, res) => {
const health = await monitor.healthCheck();
const statusCode = health.status === 'healthy' ? 200 : 503;
res.status(statusCode).json(health);
});
Performance Optimizations
1. Pipeline Operations
// redis-pipeline.js
class RedisPipelineManager {
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], index) => ({
key: keys[index],
value: err ? null : result,
error: err
}));
}
async batchSet(items) {
const pipeline = this.redis.pipeline();
items.forEach(({ key, value, ttl }) => {
if (ttl) {
pipeline.setex(key, ttl, JSON.stringify(value));
} else {
pipeline.set(key, JSON.stringify(value));
}
});
return await pipeline.exec();
}
async batchInvalidate(keys) {
if (keys.length === 0) return;
const pipeline = this.redis.pipeline();
keys.forEach(key => pipeline.del(key));
return await pipeline.exec();
}
}
// Usage
const pipelineManager = new RedisPipelineManager(redis);
// Batch operations for better performance
app.get('/api/products/batch', async (req, res) => {
const productIds = req.query.ids.split(',');
const cacheKeys = productIds.map(id => `product:${id}`);
// Get multiple products in one operation
const cached = await pipelineManager.batchGet(cacheKeys);
const missing = cached
.filter(item => !item.value)
.map(item => item.key.replace('product:', ''));
if (missing.length > 0) {
const products = await db.products.findByIds(missing);
// Cache missing products
const toCache = products.map(product => ({
key: `product:${product.id}`,
value: product,
ttl: 3600
}));
await pipelineManager.batchSet(toCache);
}
res.json(/* combined results */);
});
2. Connection Pooling
// redis-pool.js
class RedisConnectionPool {
constructor(config) {
this.pools = new Map();
this.config = config;
}
getConnection(poolName = 'default') {
if (!this.pools.has(poolName)) {
const redis = new Redis({
...this.config,
lazyConnect: true,
keepAlive: 30000,
connectTimeout: 10000,
retryDelayOnFailover: 100,
maxRetriesPerRequest: 3,
family: 4,
enableOfflineQueue: false,
});
this.pools.set(poolName, redis);
}
return this.pools.get(poolName);
}
async closeAll() {
const promises = [];
for (const [name, redis] of this.pools) {
console.log(`Closing Redis connection: ${name}`);
promises.push(redis.disconnect());
}
await Promise.all(promises);
this.pools.clear();
}
}
// Separate connections for different use cases
const pool = new RedisConnectionPool({
host: 'localhost',
port: 6379,
password: process.env.REDIS_PASSWORD
});
const cacheRedis = pool.getConnection('cache');
const sessionRedis = pool.getConnection('sessions');
const queueRedis = pool.getConnection('queues');
// Graceful shutdown
process.on('SIGTERM', async () => {
console.log('Closing Redis connections...');
await pool.closeAll();
process.exit(0);
});
เคสจริง: E-commerce Caching Strategy
// ecommerce-cache.js
class EcommerceCacheStrategy {
constructor(redis) {
this.redis = redis;
this.cache = new MultiLayerCache({ redis });
}
// Product caching with dependency tracking
async getProduct(productId) {
return await this.cache.get(
`product:${productId}`,
async () => {
const product = await db.products.findById(productId);
// Track dependencies
await this.trackDependency(`product:${productId}`, [
`category:${product.categoryId}`,
`brand:${product.brandId}`,
'all_products'
]);
return product;
}
);
}
// Category products with smart pagination
async getCategoryProducts(categoryId, page = 1, limit = 20) {
const cacheKey = `category:${categoryId}:products:${page}:${limit}`;
return await this.cache.get(cacheKey, async () => {
return await db.products.findByCategory(categoryId, {
offset: (page - 1) * limit,
limit
});
});
}
// Search results caching
async searchProducts(query, filters = {}) {
const cacheKey = `search:${this.hashQuery(query, filters)}`;
return await this.cache.get(cacheKey, async () => {
return await searchService.search(query, filters);
}, 1800); // 30 minutes TTL for search
}
// Shopping cart (session-based)
async getCart(sessionId) {
const cacheKey = `cart:${sessionId}`;
const cart = await this.redis.get(cacheKey);
return cart ? JSON.parse(cart) : { items: [], total: 0 };
}
async updateCart(sessionId, cart) {
const cacheKey = `cart:${sessionId}`;
await this.redis.setex(cacheKey, 3600, JSON.stringify(cart)); // 1 hour
}
// Price caching with frequent updates
async getPrice(productId) {
const cacheKey = `price:${productId}`;
const cached = await this.redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
const price = await db.prices.getCurrentPrice(productId);
await this.redis.setex(cacheKey, 300, JSON.stringify(price)); // 5 minutes
return price;
}
// Inventory caching with real-time updates
async getInventory(productId) {
const cacheKey = `inventory:${productId}`;
const cached = await this.redis.get(cacheKey);
if (cached) {
return parseInt(cached);
}
const quantity = await db.inventory.getQuantity(productId);
await this.redis.setex(cacheKey, 60, quantity.toString()); // 1 minute
return quantity;
}
async reserveInventory(productId, quantity) {
const cacheKey = `inventory:${productId}`;
// Atomic operation
const script = `
local current = redis.call('get', KEYS[1])
if current == false then
current = ${await db.inventory.getQuantity(productId)}
redis.call('setex', KEYS[1], 60, current)
else
current = tonumber(current)
end
if current >= tonumber(ARGV[1]) then
redis.call('decrby', KEYS[1], ARGV[1])
return current - tonumber(ARGV[1])
else
return -1
end
`;
const result = await this.redis.eval(script, 1, cacheKey, quantity);
return result >= 0 ? result : false;
}
// Helper methods
hashQuery(query, filters) {
const crypto = require('crypto');
return crypto
.createHash('md5')
.update(JSON.stringify({ query, filters }))
.digest('hex');
}
async trackDependency(key, dependencies) {
for (const dep of dependencies) {
await this.redis.sadd(`deps:${dep}`, key);
}
}
async invalidateDependencies(key) {
const dependents = await this.redis.smembers(`deps:${key}`);
if (dependents.length > 0) {
await this.redis.del(...dependents);
await this.redis.del(`deps:${key}`);
}
}
}
Troubleshooting และ Common Issues
1. Memory Management
// memory-analyzer.js
class RedisMemoryAnalyzer {
constructor(redis) {
this.redis = redis;
}
async analyzeMemoryUsage() {
const info = await this.redis.info('memory');
const keyspace = await this.redis.info('keyspace');
return {
usedMemory: this.extractValue(info, 'used_memory_human'),
peakMemory: this.extractValue(info, 'used_memory_peak_human'),
fragmentationRatio: this.extractValue(info, 'mem_fragmentation_ratio'),
totalKeys: this.extractTotalKeys(keyspace)
};
}
async findLargeKeys(limit = 10) {
const keys = await this.redis.keys('*');
const results = [];
for (const key of keys.slice(0, 100)) { // Sample first 100 keys
try {
const memory = await this.redis.memory('usage', key);
results.push({ key, memory });
} catch (error) {
// Key might have been deleted
}
}
return results
.sort((a, b) => b.memory - a.memory)
.slice(0, limit);
}
async cleanupExpiredKeys() {
const cleaned = await this.redis.eval(`
local keys = redis.call('keys', ARGV[1])
local cleaned = 0
for i=1,#keys do
local ttl = redis.call('ttl', keys[i])
if ttl == -1 then
-- Key without expiration, check if it should have one
redis.call('expire', keys[i], 3600) -- Default 1 hour
cleaned = cleaned + 1
end
end
return cleaned
`, 0, 'temp:*');
console.log(`Cleaned up ${cleaned} keys`);
return cleaned;
}
}
2. Performance Monitoring
// performance-monitor.js
class RedisPerformanceMonitor {
constructor(redis) {
this.redis = redis;
this.metrics = {
operations: 0,
totalTime: 0,
errors: 0
};
}
wrapCommand(originalMethod) {
return async (...args) => {
const start = Date.now();
try {
const result = await originalMethod.apply(this.redis, args);
this.metrics.operations++;
this.metrics.totalTime += Date.now() - start;
return result;
} catch (error) {
this.metrics.errors++;
throw error;
}
};
}
getMetrics() {
return {
...this.metrics,
averageLatency: this.metrics.operations > 0
? this.metrics.totalTime / this.metrics.operations
: 0,
errorRate: this.metrics.operations > 0
? this.metrics.errors / this.metrics.operations
: 0
};
}
reset() {
this.metrics = { operations: 0, totalTime: 0, errors: 0 };
}
}
// Usage
const monitor = new RedisPerformanceMonitor(redis);
// Wrap Redis commands
redis.get = monitor.wrapCommand(redis.get.bind(redis));
redis.set = monitor.wrapCommand(redis.set.bind(redis));
// Regular reporting
setInterval(() => {
console.log('Redis Performance:', monitor.getMetrics());
monitor.reset();
}, 60000); // Every minute
สรุป: Redis ที่เปลี่ยนวิธีคิดเรื่อง Caching
ก่อนเข้าใจ Redis จริงๆ:
- Cache Stampede ทำเซิร์ฟเวอร์ล่มบ่อย
- Caching strategy ไม่มี
- Performance ไม่สม่ำเสมอ
- Memory leaks จาก cache ไม่หมดอายุ
หลังเรียนรู้ Redis อย่างจริงจัง:
- Cache hit rate > 95%
- Response time เร็วและสม่ำเสมอ
- Zero cache stampede incidents
- Memory usage controlled
ข้อดีที่ได้จริง:
- Performance ดีขึ้น 10 เท่า (200ms → 20ms)
- Scalability รองรับ traffic เยอะขึ้น 50 เท่า
- Reliability system stability เพิ่มขึ้นมาก
- Cost efficiency ลด database load 80%
บทเรียนสำคัญ:
- Caching ไม่ใช่แค่เก็บข้อมูล แต่เป็น architectural decision
- Cache invalidation คือปัญหาที่ยากที่สุดใน computer science
- Monitoring สำคัญมาก ต้องรู้ว่า cache ทำงานยังไง
- Graceful degradation เมื่อ cache fail ระบบยังต้องทำงานได้
Anti-patterns ที่ต้องหลีกเลี่ยง:
- Cache without TTL
- ไม่มี cache warming strategy
- Synchronous cache operations ใน critical path
- ไม่ handle cache failures
Redis มันเหมือน เครื่องยนต์ turbo ของ web application
ถ้าใช้เป็น มันจะทำให้ระบบเร็วขึ้นอย่างมหาศาล
แต่ถ้าใช้ผิด อาจจะระเบิดได้! 💥🚀
ตอนนี้เข้าใจแล้วว่าทำไม Redis ถึงเป็น backbone ของ modern applications เพราะมันไม่ใช่แค่ cache แต่เป็น data structure server ที่ทรงพลังมาก! ⚡