Cache strategy
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:
-
Time-Based Expiration (TTL)
- Set a fixed expiration time
- Suitable for infrequently changing data
-
Write-Invalidation
- Actively clear the cache when data changes
- Suitable for scenarios requiring strong consistency
-
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:
- 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);
}
- 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:
-
Key metrics to monitor:
- Cache hit rate
- Average response time
- Memory usage
-
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;
}
}
- 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
- 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));
}
- 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()));
});
});
}
- 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