article

Redis ที่ทำให้ผมเข้าใจว่า Caching ไม่ใช่แค่เก็บข้อมูล

18 min read

วันที่เซิร์ฟเวอร์ล่มเพราะ 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 ที่ทรงพลังมาก! ⚡