Redis 使用场景:缓存、队列和限流
你构建的 Web 应用在用户量少时运行良好,但随着流量增长,你会遇到页面加载缓慢、后台任务重复执行以及 API 被滥用客户端频繁请求的问题。这些都是典型的扩展性问题,而 Redis 可以优雅地解决它们。Redis 是一个内存数据存储,擅长三项核心任务:缓存、队列和限流。在本文中,我们将通过实际示例和最佳实践来探讨每个使用场景。
为什么在这些场景中使用 Redis?
Redis 将数据存储在内存中,因此速度极快——通常响应时间在亚毫秒级。它支持丰富的数据结构,如字符串、列表、集合、有序集合和哈希,这些结构天然适用于缓存、队列和限流。它还提供原子操作、发布/订阅和持久化选项,使其成为后端开发人员的多功能工具。
1. 使用 Redis 进行缓存
缓存是 Redis 最常见的用例。通过将频繁访问的数据存储在 Redis 中,可以减少数据库负载并加快响应速度。
缓存的工作原理
当请求到达时,检查数据是否存在于 Redis 中。如果存在(缓存命中),直接返回。如果不存在(缓存未命中),从数据库获取数据,将其存储到 Redis 并设置过期时间,然后返回。这种模式称为缓存旁路(cache-aside)。
// Node.js 示例,使用 ioredis
const Redis = require('ioredis');
const redis = new Redis();
async function getUser(userId) {
const cacheKey = `user:${userId}`;
const cached = await redis.get(cacheKey);
if (cached) return JSON.parse(cached);
const user = await db.query('SELECT * FROM users WHERE id = ?', [userId]);
await redis.set(cacheKey, JSON.stringify(user), 'EX', 3600); // 1 小时 TTL
return user;
}
缓存的最佳实践
- 设置 TTL:始终设置过期时间,以避免数据过时和内存膨胀。
- 使用适当的数据结构:对于简单的键值对,使用字符串。对于对象,使用哈希以便单独更新字段。
- 处理缓存失效:当底层数据更改时,更新或删除缓存条目。
- 监控命中率:命中率低意味着缓存效果不佳;调整键或 TTL。
缓存失效策略
失效处理很困难。常见的策略包括:
- 基于时间:依赖 TTL。简单但可能提供过时数据。
- 写穿透(Write-through):每当更新数据库时更新缓存。
- 写回(Write-behind):先更新缓存,然后异步更新数据库(存在数据丢失风险)。
2. 使用 Redis 实现队列
队列将耗时的任务与请求-响应周期解耦。例如,发送电子邮件、处理图像或生成报告可以异步完成。
实现简单队列
Redis 列表非常适合队列。使用 LPUSH 将任务添加到左侧,使用 BRPOP 阻塞并从右侧弹出(FIFO)。
// 生产者:添加任务
await redis.lpush('email_queue', JSON.stringify({ to: 'user@example.com', subject: 'Welcome' }));
// 消费者:处理任务(在工作进程中)
while (true) {
const job = await redis.brpop('email_queue', 0); // 0 = 无限阻塞
const { to, subject } = JSON.parse(job[1]);
await sendEmail(to, subject);
}
使用 BRPOPLPUSH 实现可靠队列
为了避免工作进程崩溃时丢失任务,使用 BRPOPLPUSH 原子地将任务移动到处理列表。成功处理后,将其从处理列表中移除。如果工作进程死亡,另一个工作进程可以从处理列表中恢复任务。
const job = await redis.brpoplpush('email_queue', 'processing_queue', 0);
try {
// 处理任务
await redis.lrem('processing_queue', 1, job);
} catch (err) {
// 处理错误,可能重新入队
}
专用队列库
在生产环境中,考虑使用 Bull(Node.js)、RQ(Python)或 Sidekiq(Ruby)等库。它们提供重试、调度和监控功能。
3. 使用 Redis 进行限流
限流保护你的 API 免受滥用并确保公平使用。Redis 的原子操作使其非常适合计数请求。
固定窗口限流
在每个时间窗口内为每个用户递增计数器。如果计数超过限制,则拒绝请求。
async function isAllowed(userId, limit = 100, windowSec = 60) {
const key = `rate:${userId}:${Math.floor(Date.now() / 1000 / windowSec)}`;
const count = await redis.incr(key);
if (count === 1) await redis.expire(key, windowSec);
return count