
为什么使用 Redis 缓存?
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
}
缓存雪崩预防(使用分布式锁)
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
}
限流
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 可使所有缓存数据失效