阿里云主机折上折
  • 微信号
Current Site:Index > Cache strategy

Cache strategy

Author:Chuan Chen 阅读数:15714人阅读 分类: Node.js

Basic Concepts of Caching Strategies

Caching strategies are one of the key methods to enhance application performance by storing frequently accessed data in high-speed storage media, reducing reliance on slower data sources (such as databases or remote APIs). In Node.js, caching can significantly reduce I/O overhead, especially when handling high-concurrency requests.

Common types of caches include:

  • In-memory caching (e.g., Node.js Map objects or dedicated libraries)
  • Distributed caching (e.g., Redis)
  • Browser caching (controlled via HTTP headers)
  • CDN caching (for static resources)

In-Memory Cache Implementation

The simplest cache implementation in Node.js uses in-memory storage. Below is a basic cache example based on Map:

class SimpleCache {
  constructor() {
    this.cache = new Map();
    this.ttl = 60000; // Default 1-minute expiration time
  }

  set(key, value, ttl = this.ttl) {
    const expiresAt = Date.now() + ttl;
    this.cache.set(key, { value, expiresAt });
  }

  get(key) {
    const item = this.cache.get(key);
    if (!item) return null;
    
    if (Date.now() > item.expiresAt) {
      this.cache.delete(key);
      return null;
    }
    return item.value;
  }

  delete(key) {
    this.cache.delete(key);
  }

  clear() {
    this.cache.clear();
  }
}

Usage example:

const cache = new SimpleCache();
cache.set('user:123', { name: 'Alice', role: 'admin' });

// Fetch after 5 seconds
setTimeout(() => {
  console.log(cache.get('user:123')); // Returns cached object
}, 5000);

// Fetch after 2 minutes
setTimeout(() => {
  console.log(cache.get('user:123')); // Returns null (expired)
}, 120000);

Cache Invalidation Strategies

Proper cache invalidation mechanisms are crucial for ensuring data consistency. Common strategies include:

  1. Time-Based Expiration (TTL)

    • Set a fixed expiration time
    • Suitable for infrequently changing data
  2. Write-Invalidation

    • Actively clear the cache when data changes
    • Suitable for scenarios requiring strong consistency
  3. LRU (Least Recently Used)

    • Evict the least recently used entries when the cache reaches capacity
    • Node.js implementation example:
class LRUCache {
  constructor(capacity = 100) {
    this.capacity = capacity;
    this.cache = new Map();
  }

  get(key) {
    if (!this.cache.has(key)) return null;
    
    const value = this.cache.get(key);
    this.cache.delete(key);
    this.cache.set(key, value);
    return value;
  }

  set(key, value) {
    if (this.cache.has(key)) {
      this.cache.delete(key);
    } else if (this.cache.size >= this.capacity) {
      // Delete the least recently used entry
      const oldestKey = this.cache.keys().next().value;
      this.cache.delete(oldestKey);
    }
    this.cache.set(key, value);
  }
}

Multi-Level Cache Architecture

For high-performance applications, a multi-level caching strategy can be adopted:

graph TD
    A[Client] -->|Request| B[Browser Cache]
    B -->|Miss| C[CDN Cache]
    C -->|Miss| D[Application Memory Cache]
    D -->|Miss| E[Redis Cache]
    E -->|Miss| F[Database]

Code structure for implementing multi-level caching in Node.js:

async function getWithMultiLevelCache(key) {
  // 1. Check in-memory cache
  let data = memoryCache.get(key);
  if (data) return data;

  // 2. Check Redis cache
  data = await redisClient.get(key);
  if (data) {
    memoryCache.set(key, data); // Backfill in-memory cache
    return data;
  }

  // 3. Query the database
  data = await db.query('SELECT * FROM data WHERE key = ?', [key]);
  
  // Write to all cache levels
  await redisClient.setex(key, 3600, data); // Redis cache for 1 hour
  memoryCache.set(key, data); // In-memory cache
  
  return data;
}

Cache Breakdown and Avalanche Protection

Cache Breakdown Solutions

When a hot key expires, a large number of requests hit the database directly:

async function getProduct(id) {
  const cacheKey = `product:${id}`;
  let product = await redis.get(cacheKey);
  
  if (!product) {
    // Use a mutex lock to prevent concurrent rebuilds
    const lockKey = `lock:${cacheKey}`;
    const lock = await redis.set(lockKey, '1', 'EX', 10, 'NX');
    
    if (lock) {
      try {
        product = await db.getProduct(id);
        await redis.setex(cacheKey, 3600, product);
      } finally {
        await redis.del(lockKey);
      }
    } else {
      // Wait for another process to rebuild the cache
      await new Promise(resolve => setTimeout(resolve, 100));
      return getProduct(id); // Retry
    }
  }
  
  return product;
}

Cache Avalanche Protection

A large number of keys expiring simultaneously can overload the database:

  1. Randomize expiration times:
function setWithRandomTtl(key, value, baseTtl) {
  const ttl = baseTtl + Math.floor(Math.random() * 60 * 1000); // Add random 0-60 seconds
  redis.setex(key, ttl, value);
}
  1. Never expire + background updates:
// Set a never-expiring key
redis.set('hot:data', data);

// Background periodic updates
setInterval(async () => {
  const newData = await fetchLatestData();
  redis.set('hot:data', newData);
}, 5 * 60 * 1000); // Update every 5 minutes

HTTP Caching Strategies

Implementing HTTP cache headers in Node.js:

const express = require('express');
const app = express();

// Static resource caching
app.use('/static', express.static('public', {
  maxAge: '1y', // Browser cache for 1 year
  immutable: true // Content is immutable
}));

// API response caching
app.get('/api/data', async (req, res) => {
  const data = await getData();
  
  // Set cache headers
  res.set({
    'Cache-Control': 'public, max-age=300', // 5 minutes
    'ETag': generateETag(data) // Generate ETag based on content
  });
  
  res.json(data);
});

// Conditional GET request handling
app.get('/api/data', (req, res) => {
  const ifNoneMatch = req.get('If-None-Match');
  const currentETag = generateETag(data);
  
  if (ifNoneMatch === currentETag) {
    return res.status(304).end(); // Not modified
  }
  
  // ...Return new data
});

Cache Monitoring and Optimization

An effective caching system requires continuous monitoring:

  1. Key metrics to monitor:

    • Cache hit rate
    • Average response time
    • Memory usage
  2. Simple monitoring implementation in Node.js:

class MonitoredCache extends SimpleCache {
  constructor() {
    super();
    this.stats = {
      hits: 0,
      misses: 0,
      size: 0
    };
  }

  get(key) {
    const value = super.get(key);
    if (value) {
      this.stats.hits++;
    } else {
      this.stats.misses++;
    }
    this.stats.size = this.cache.size;
    return value;
  }

  getHitRate() {
    const total = this.stats.hits + this.stats.misses;
    return total > 0 ? (this.stats.hits / total) : 0;
  }
}
  1. Using Redis monitoring commands:
# Check memory usage
redis-cli info memory

# Check keyspace statistics
redis-cli info keyspace

# Monitor hit rate
redis-cli info stats | grep -E '(keyspace_hits|keyspace_misses)'

Cache Pattern Practices

Write-Through Pattern

async function writeThrough(key, value) {
  // 1. Update the database first
  await db.update(key, value);
  
  // 2. Update the cache
  await cache.set(key, value);
}

Write-Behind Pattern

async function writeBack(key, value) {
  // 1. Only update the cache
  await cache.set(key, value);
  
  // 2. Asynchronously batch-update the database
  if (!this.batchUpdate) {
    this.batchUpdate = setTimeout(async () => {
      const dirtyKeys = cache.getDirtyKeys();
      await db.bulkUpdate(dirtyKeys);
      this.batchUpdate = null;
    }, 5000); // Batch update every 5 seconds
  }
}

Cache Warming

Load hot data at startup:

async function warmUpCache() {
  const hotProducts = await db.query(`
    SELECT * FROM products 
    WHERE views > 1000 
    ORDER BY views DESC 
    LIMIT 100
  `);
  
  await Promise.all(
    hotProducts.map(p => 
      redis.setex(`product:${p.id}`, 86400, JSON.stringify(p))
    )
  );
}

// During service startup
app.listen(3000, () => {
  warmUpCache().then(() => {
    console.log('Cache warming completed');
  });
});

Special Scenario Handling

Paginated Query Caching

async function getProductsPage(page, size) {
  const cacheKey = `products:page:${page}:size:${size}`;
  const cached = await redis.get(cacheKey);
  
  if (cached) return JSON.parse(cached);
  
  const products = await db.query(
    'SELECT * FROM products LIMIT ? OFFSET ?',
    [size, (page - 1) * size]
  );
  
  // Set a shorter expiration time since paginated data changes frequently
  await redis.setex(cacheKey, 300, JSON.stringify(products));
  
  return products;
}

Related Data Caching

For related data, a lazy-loading strategy can be used:

async function getUserWithPosts(userId) {
  const userKey = `user:${userId}`;
  const postsKey = `user:${userId}:posts`;
  
  const [user, posts] = await Promise.all([
    redis.get(userKey),
    redis.get(postsKey)
  ]);
  
  if (!user) {
    const userData = await db.getUser(userId);
    await redis.setex(userKey, 3600, JSON.stringify(userData));
    user = userData;
  } else {
    user = JSON.parse(user);
  }
  
  if (!posts) {
    const postsData = await db.getUserPosts(userId);
    await redis.setex(postsKey, 1800, JSON.stringify(postsData));
    posts = postsData;
  } else {
    posts = JSON.parse(posts);
  }
  
  return { ...user, posts };
}

Performance Optimization Techniques

  1. Batch Operations:
// Batch cache retrieval
async function batchGet(keys) {
  if (redis) {
    return redis.mget(keys);
  }
  
  // In-memory cache implementation
  return keys.map(key => this.cache.get(key));
}
  1. Cache Data Compression:
const zlib = require('zlib');

async function setCompressed(key, value) {
  const compressed = await new Promise((resolve, reject) => {
    zlib.deflate(JSON.stringify(value), (err, buffer) => {
      err ? reject(err) : resolve(buffer);
    });
  });
  await redis.set(key, compressed);
}

async function getCompressed(key) {
  const compressed = await redis.getBuffer(key);
  return new Promise((resolve, reject) => {
    zlib.inflate(compressed, (err, buffer) => {
      if (err) return reject(err);
      resolve(JSON.parse(buffer.toString()));
    });
  });
}
  1. Cache Partitioning:
// Shard by business logic
const SHARD_COUNT = 16;
function getShard(key) {
  const hash = simpleHash(key);
  return `cache:shard:${hash % SHARD_COUNT}`;
}

function simpleHash(str) {
  let hash = 0;
  for (let i = 0; i < str.length; i++) {
    hash = ((hash << 5) - hash) + str.charCodeAt(i);
    hash |= 0; // Convert to 32-bit integer
  }
  return Math.abs(hash);
}

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

上一篇:垃圾回收机制

下一篇:负载测试

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.