正在加载,请稍候…

Node.js 中的 Redis 缓存模式:旁路缓存、直写缓存等

在 Node.js 中实现生产级 Redis 缓存——旁路缓存模式、直写缓存、缓存失效、分布式锁、限流和发布/订阅。

Node.js 中的 Redis 缓存模式:旁路缓存、直写缓存等

为什么使用 Redis 缓存?

Redis 位于应用和数据库之间,从内存中提供频繁读取的数据,延迟为微秒级,而非毫秒级的数据库查询。

Node.js 中的 Redis 缓存模式:旁路缓存、直写缓存等 示意图

安装与配置

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))

旁路缓存模式

async function getUser(id: string): Promise<User | null> {
  const cacheKey = `user:${id}`
  
  // 1. 检查缓存
  const cached = await redis.get(cacheKey)
  if (cached) {
    return JSON.parse(cached)
  }
  
  // 2. 缓存未命中——查询数据库
  const user = await db.users.findById(id)
  if (!user) return null
  
  // 3. 存入缓存并设置 TTL
  await redis.set(cacheKey, JSON.stringify(user), 'EX', 3600) // 1 小时
  
  return user
}

// 更新时失效缓存
async function updateUser(id: string, data: Partial<User>) {
  const user = await db.users.update(id, data)
  await redis.del(`user:${id}`) // 失效缓存
  return user
}

Node.js 中的 Redis 缓存模式:旁路缓存、直写缓存等 示意图

缓存雪崩预防(使用分布式锁)

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)
  
  // 获取锁以防止多次获取
  const lock = await redlock.acquire([`lock:${key}`], 5000)
  
  try {
    // 获取锁后再次检查
    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()
  }
}

直写缓存模式

async function createPost(data: CreatePostDto): Promise<Post> {
  // 同时写入数据库和缓存
  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), // 只保留最近 100 条
  ])
  
  return post
}

Node.js 中的 Redis 缓存模式:旁路缓存、直写缓存等 示意图

限流

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 中间件
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()
})

发布/订阅实现实时通信

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

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

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

有序集合实现排行榜

// 添加分数
await redis.zadd('leaderboard', score, userId)

// 获取前 10 名
const top10 = await redis.zrevrange('leaderboard', 0, 9, 'WITHSCORES')

// 获取用户排名
const rank = await redis.zrevrank('leaderboard', userId)

缓存键策略

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')}`,
}

// 为键添加版本号——递增 v1 -> v2 可使所有缓存数据失效