首页 / 文章 / 超越缓存的Redis:Node.js中的会话管理、速率限制、队列以及发布/订阅功能

超越缓存的Redis:Node.js中的会话管理、速率限制、队列以及发布/订阅功能

适用于 Node.js 后端的七种 Redis 模式——带 TTL 的缓存、OTP 密钥、速率限制、BullMQ 任务、会话管理、Pub/Sub 限制,以及何时不应使用 Redis。

1424 词

早期的思维模式将 Redis 视为“仅仅是缓存”:存储一个值,设置过期时间,读取速度比数据库快,仅此而已。

这种看法并没有错,但并不完整。

实际上,Redis 被用于处理高频 API 响应、共享会话、滥用计数器、一次性验证码、延迟任务队列、轻量级实时推送以及其他短生命周期的数据。

更好的理解方式是:将 Redis 视为具有多种后端功能的快速内存数据存储,缓存只是其中的第一项功能。

1. Redis 能显著提升 API 的速度

从缓存开始入手。

以 GET /products 为例。

如果没有缓存,每个请求的流程可能如下:

客户端 → Node.js API → PostgreSQL → Node.js API → 客户端

当请求次数达到数千次时,昂贵的查询会导致数据库重复执行相同的工作。

Redis 可以置于该查询之前。

客户端 → Node.js API → Redis

查询命中时立即返回结果;未命中时则查询 PostgreSQL,将结果存储在 Redis 中后再返回。

一个简单的 ioredis 示例:

import Redis from "ioredis";
const redis = new Redis(process.env.REDIS_URL);
async function getProducts() {
  const cached = await redis.get("products");
  if (cached) {
    return JSON.parse(cached);
  }
  const products = await database.product.findMany();
  await redis.set(
    "products",
    JSON.stringify(products),
    "EX",
    300
  );
  return products;
}h

这里的 EX 表示缓存数据在300秒后过期。

仍然存在一个难题:**缓存失效问题**。

假设 Redis 中存储着 product:123 → price:500,而 PostgreSQL 中的数值却是600。虽然数据库中的数据是正确的,但 Redis 可能仍会返回500。

缓存并不意味着“把所有数据都存入 Redis”。团队仍需考虑以下问题:

  • 生存时间
  • 缓存失效机制
  • 过期数据
  • 查询未命中情况
  • 缓存故障

将数据存入 Redis 很简单,但保持其准确性则更为困难。

2. Redis 非常适合存储临时数据

短期有效的值非常适合用于:一次性登录码、重置链接、验证令牌、临时会话数据、滥用计数器以及警告锁。

示例:生成一次性密码:

const otp = "482913";
await redis.set(
  `otp:${userId}`,
  otp,
  "EX",
  300
);

该一次性密码会在五分钟后自动过期。

无需专门的OTP表格,也不需要额外的清理任务。

可通过以下方式读取该密码:

const otp = await redis.get(`otp:${userId}`);

在过期时间到达后,Redis会根据其过期机制删除对应键值。

正是这种特性使得Redis成为存储短期应用数据的理想选择。

3. Redis可帮助实现速率限制

以POST /login为例。

如果没有限制,客户端可能会频繁尝试登录——连续发送数千次请求。

Redis可以为每个客户端维护一个计数器:

const key = `login-attempts:${ip}`;
const attempts = await redis.incr(key);
if (attempts === 1) {
  await redis.expire(key, 60);
}
if (attempts > 10) {
  throw new Error("Too many requests");
}

其结构为:IP地址 → Redis计数器 → 当前计数值 → 限制次数。

当负载均衡器后存在多个 API 服务器时,这一点尤为重要。每个进程内的内存计数器并非全局共享的,而 Redis 可以为这些服务器提供统一的存储空间。

4. Redis 可用于处理后台任务

创建账户时可能需要生成用户、发送欢迎邮件、生成数据、通知其他服务以及执行其他操作。

HTTP 请求不应等待所有这些操作完成。

应改为将任务推送到队列中:

客户端 → API → 队列 → 响应

随后:

队列 → 工作进程 → 处理任务

BullMQ 是一种常用的基于 Redis 的 Node.js 工具:

await emailQueue.add("welcome-email", {
  userId: user.id,
  email: user.email,
});

工作进程会独立处理任务:

const worker = new Worker(
  "email",
  async (job) => {
    if (job.name === "welcome-email") {
      await sendWelcomeEmail(job.data.email);
    }
  },
  {
    connection: redisConnection,
  }
);

API 在回复用户之前无需等待邮件服务完成处理。

这种方法适用于那些处理速度较慢、可重试、依赖外部服务、需要大量 CPU 资源,或无需在 API 响应前就完成的任务。

分工明确:API负责处理请求,工作进程则承担繁重任务。

5. Redis可用于存储会话

会话管理也是Redis的适用场景。诸如session:abc123这样的键可能存储以下内容:

{
  "userId": "123",
  "role": "ADMIN"
}

当多个后端实例通过Redis共享同一个会话存储时,这种方式就显得非常有用。

重要区别:Redis不会自动提升认证的安全性。

团队仍需处理会话标识符、安全Cookie、过期机制、必要的CSRF防护、认证以及授权问题。

Redis只是基础设施,而非安全策略。

6. Redis有助于实现实时功能

Pub/Sub机制可以将事件分发到各个实例。其工作流程大致为:客户端向服务器A发送请求,将数据发布到Redis,服务器B上的订阅处理程序接收到消息后,再将其传递给另一客户端。

插图:

await redis.publish(
  "notifications",
  JSON.stringify({
    userId: "123",
    message: "Your order has shipped",
  })
);
await subscriber.subscribe("notifications")
subscriber.on("message", (channel, message) => {
  console.log(channel, message);
});

适用于通知、实时更新、聊天相关流程以及事件传播。

局限性:Redis Pub/Sub 并非持久化消息队列。

如果需要持久化处理、重试机制或保证消息送达,应根据具体情况选择队列或 Redis Streams。

了解这一区别非常重要。

7. 如果到处都使用 Redis,它就会成为问题

最重要的教训是:一旦有了 Redis,人们就容易想把所有数据都存进去。

切勿如此。

仅仅因为速度快,并不意味着所有数据都应该存放在内存中。

PostgreSQL 可以继续作为业务数据的永久可信来源,而 Redis 则负责处理缓存、会话、一次性密码、速率限制以及队列等任务。

合理的划分方式是将重要的业务数据存储在 PostgreSQL 中,而将 Redis 用于高频访问的数据、短过期时间的需求,以及队列或计数器等辅助工作负载。

尽早明确这种划分能避免后续的大量重新设计。

Redis 数据结构很重要

Redis 不仅仅是 键 → 字符串 的结构,它还提供了多种数据结构。

字符串

用于存储简单值,例如 user:123:name → “Mit”。

哈希表

在一个键下存储多个字段:

user:123
name → Mit
role → ADMIN
email → example@email.com

列表

有序集合,可用于实现类似队列的功能。

集合

存储唯一值。

有序集合

根据分数对元素进行排序——例如排行榜:

1000 → Player A
900  → Player B
800  → Player C

选择合适的数据结构往往能简化问题。

需避免的常见 Redis 错误

错误 1:将所有内容都缓存

并非每个查询都需要缓存。缓存会增加复杂性。如果某个查询的速度已经足够快,使用Redis反而可能解决并不存在的问题。

错误2:未设置过期时间

没有设置TTL的临时数据会不断积累。如果数据无需永久保存,就应该为其设定过期策略。

错误3:将Redis当作永久数据库

如果关键业务数据的唯一副本存储在Redis中,系统就会存在严重的依赖性。必须明确数据的真实来源。

错误4:忽视Redis故障

需要提前规划Redis宕机时的应对措施。对于许多缓存应用而言,回退到数据库是可行的方案。具体策略取决于Redis在系统中的角色。

错误5:在不了解工作负载的情况下使用Redis

速度并非无限。仍需考虑内存管理、数据驱逐、连接问题、键设计、TTL设置、序列化方式、网络延迟以及持久化需求。

Redis如何改变后端设计思路

许多设计最初都是让请求处理程序直接与SQL数据库交互。

一旦出现内存存储和延迟执行的工作进程,处理流程往往会变得更复杂:处理程序会先查询Redis,再查询数据库;或者将任务放入队列,由工作进程去联系外部服务提供商。

后端系统逐渐变成由各种专用组件构成的集合。而Redis则是能够在多个角色中发挥作用的组件之一。

结语

不要仅仅因为别人都在用就采用Redis。

在需求明确时再选用相应方案:热数据读取使用缓存,短期有效的密钥采用TTL机制,设置计数器限制滥用行为,延迟任务则用队列处理,多个实例间共享会话存储,若只需轻量级消息分发则可使用Pub/Sub。

避免将Redis当作解决所有后端问题的默认方案。

优秀的系统并非拥有最长的工具列表,而是每个工具都有其存在的必要。