正在加载,请稍候…

Redis Caching Patterns in Node.js: Cache-Aside, Write-Through, and More

Implement production Redis caching in Node.js — cache-aside pattern, write-through, cache invalidation, distributed locks, rate limiting, and pub/sub.

Why Redis Caching?

Redis sits between your app and database, serving frequent reads from memory at microsecond latency instead of millisecond DB queries.

Setup

npm install ioredis
import Redis from 'ioredis'

const redis = new Redis({
  host: process.env.REDIS_HOST,
  port: 6379,
  password: process.env.REDIS_PASSWORD,
  enableReadyCheck: true,
  maxRetriesPerRequest: 3,
  lazyConnect: true,
})

redis.on('error', (err) => console.error('Redis error:', err))

Cache-Aside Pattern

async function getUser(id: string): Promise<User | null> {
  const cacheKey = `user:${id}`
  
  // 1. Check cache
  const cached = await redis.get(cacheKey)
  if (cached) {
    return JSON.parse(cached)
  }
  
  // 2. Cache miss — query DB
  const user = await db.users.findById(id)
  if (!user) return null
  
  // 3. Store in cache with TTL
  await redis.set(cacheKey, JSON.stringify(user), 'EX', 3600) // 1 hour
  
  return user
}

// Invalidate on update
async function updateUser(id: string, data: Partial<User>) {
  const user = await db.users.update(id, data)
  await redis.del(`user:${id}`) // Invalidate cache
  return user
}

Cache Stampede Prevention (with Distributed Lock)

import Redlock from 'redlock'

const redlock = new Redlock([redis], {
  retryCount: 10,
  retryDelay: 200,
})

async function getCachedData(key: string, fetchFn: () => Promise<any>) {
  const cached = await redis.get(key)
  if (cached) return JSON.parse(cached)
  
  // Acquire lock to prevent multiple fetches
  const lock = await redlock.acquire([`lock:${key}`], 5000)
  
  try {
    // Double-check after acquiring lock
    const recheck = await redis.get(key)
    if (recheck) return JSON.parse(recheck)
    
    const data = await fetchFn()
    await redis.set(key, JSON.stringify(data), 'EX', 3600)
    return data
  } finally {
    await lock.release()
  }
}

Write-Through Pattern

async function createPost(data: CreatePostDto): Promise<Post> {
  // Write to DB and cache simultaneously
  const post = await db.posts.create(data)
  
  await Promise.all([
    redis.set(`post:${post.id}`, JSON.stringify(post), 'EX', 86400),
    redis.lpush('posts:recent', post.id),
    redis.ltrim('posts:recent', 0, 99), // Keep only 100 recent
  ])
  
  return post
}

Rate Limiting

async function rateLimit(userId: string, limit = 100, windowSecs = 60): Promise<boolean> {
  const key = `ratelimit:${userId}:${Math.floor(Date.now() / 1000 / windowSecs)}`
  
  const count = await redis.incr(key)
  if (count === 1) {
    await redis.expire(key, windowSecs)
  }
  
  return count <= limit
}

// Express middleware
app.use(async (req, res, next) => {
  const allowed = await rateLimit(req.user?.id ?? req.ip)
  if (!allowed) return res.status(429).json({ error: 'Rate limit exceeded' })
  next()
})

Pub/Sub for Real-Time

const publisher = new Redis()
const subscriber = new Redis()

// Publisher
await publisher.publish('notifications', JSON.stringify({
  userId: '123',
  message: 'Your order shipped!',
}))

// Subscriber
await subscriber.subscribe('notifications')
subscriber.on('message', (channel, message) => {
  const notification = JSON.parse(message)
  socketServer.to(notification.userId).emit('notification', notification)
})

Sorted Sets for Leaderboards

// Add score
await redis.zadd('leaderboard', score, userId)

// Get top 10
const top10 = await redis.zrevrange('leaderboard', 0, 9, 'WITHSCORES')

// Get user rank
const rank = await redis.zrevrank('leaderboard', userId)

Cache Key Strategy

const keys = {
  user: (id: string) => `user:v1:${id}`,
  userPosts: (id: string, page: number) => `user:${id}:posts:p${page}`,
  product: (id: string) => `product:v1:${id}`,
  search: (query: string) => `search:${Buffer.from(query).toString('base64')}`,
}

// Version your keys — increment v1 -> v2 to invalidate all cached data