Skip to content

JS 全栈开发者的 Redis 学习实战指南

更新: 6/28/2026 字数: 0 字 时长: 0 分钟

不讲 Redis 集群运维、不扯持久化底层原理,只讲一件事:你用 Node.js 写全栈项目时,Redis 到底怎么帮你做缓存、限流、队列和实时同步。

一、入门:Redis 是什么,全栈为什么需要它

Redis 是 JS 全栈的高速内存缓存

一句话:Redis 是一个跑在内存里的、超快的键值存储。你可以把它理解成"一个所有 Node 进程都能共享访问的、带过期时间的全局 Map,而且重启后还能选择不丢"。

为什么 JS 全栈离不开它?因为有些活儿数据库干起来太慢、太重:

  • 数据库读一次几毫秒到几十毫秒,Redis 读一次通常是微秒级。热点数据放 Redis,接口快一个数量级。
  • 你的 Node 服务一旦多开几个实例(PM2 cluster、多容器),进程内的内存变量就不共享了——用户在实例 A 登录,请求打到实例 B 就不认识他。Redis 作为外部共享存储正好解决这个问题。

连接 Redis:用 ioredis

Node 生态首选 ioredis(比老的 node_redis 更好用,原生支持 Promise、TS、集群)。

bash
npm install ioredis
typescript
// redis.ts —— 全项目共用一个实例
import Redis from 'ioredis';

export const redis = new Redis({
  host: process.env.REDIS_HOST || '127.0.0.1',
  port: 6379,
  password: process.env.REDIS_PASSWORD,
  // 关键:连不上时的重试策略,别让服务卡死
  retryStrategy: (times) => Math.min(times * 50, 2000),
});

redis.on('error', (e) => console.error('[Redis] 连接出错', e));

第一坑别在每次请求里 new Redis()。Redis 连接要复用,全项目维护一个单例(像上面这样导出),否则连接数爆炸。

最常用的几种数据结构(够全栈用了)

Redis 数据类型很多,但你日常 90% 只用这几种:

typescript
// String:最常用,存单值/JSON/计数
await redis.set('user:1:name', '张三');
await redis.set('user:1', JSON.stringify({ id: 1, name: '张三' })); // 存对象要序列化
const raw = await redis.get('user:1');
const user = raw ? JSON.parse(raw) : null;                          // 取出要反序列化

// 带过期时间(秒)—— 缓存的灵魂
await redis.set('captcha:138xxxx', '8888', 'EX', 300); // 5分钟后自动消失

// Hash:存对象的各字段,可单独读写某个字段
await redis.hset('user:1', { name: '张三', age: '18' });
await redis.hget('user:1', 'name');

// 计数 incr:原子自增,限流/计数器用
await redis.incr('api:count');

// List:当队列用(左进右出)
await redis.lpush('tasks', 'job1');
await redis.rpop('tasks');

// Set / Sorted Set:去重、排行榜
await redis.zadd('rank', 100, 'userA'); // 排行榜

第二坑(必记)Redis 只认字符串/Buffer。存 JS 对象必须 JSON.stringify,取出来 JSON.parse。直接 set('key', {a:1}) 会被存成 [object Object],取出来就废了。

二、进阶:把 Redis 当缓存层用

进阶的核心就一个模式——Cache-Aside(旁路缓存),几乎所有"加缓存"的需求都是它:

读数据:先查 Redis,命中就直接返回;没命中再查数据库,然后把结果写回 Redis 并设过期时间。

封装成一个通用工具函数,全栈项目里到处能用:

typescript
/**
 * 通用缓存读取:缓存优先,未命中回源并回填
 * @param key 缓存键
 * @param ttl 过期秒数
 * @param fetcher 缓存未命中时怎么从数据库取
 */
async function cacheGet<T>(key: string, ttl: number, fetcher: () => Promise<T>): Promise<T> {
  const cached = await redis.get(key);
  if (cached) return JSON.parse(cached);      // 命中

  const data = await fetcher();               // 未命中,回源查库
  await redis.set(key, JSON.stringify(data), 'EX', ttl); // 回填并设过期
  return data;
}

// 用起来非常清爽
app.get('/api/product/:id', async (req, res) => {
  const product = await cacheGet(
    `product:${req.params.id}`,
    600, // 缓存10分钟
    () => db.product.findById(req.params.id) // 你的 ORM 查询
  );
  res.json(product);
});

更新数据时,记得让缓存失效(删除而非改写,下次读自动回填):

typescript
async function updateProduct(id: string, data: any) {
  await db.product.update(id, data);
  await redis.del(`product:${id}`); // 删缓存,保证下次读到新数据
}

第三坑一定要设过期时间(TTL)。不设 TTL 的缓存会一直占内存,且数据更新后容易留下脏数据。除非是明确要长期存的,否则 set 必带 EX

三、实战场景一:会话缓存(Session / 登录态)

实战一:用 Redis 做会话缓存

这是全栈最高频的 Redis 用途。无论你用 session 还是 token,登录态存 Redis 能解决"多实例共享"和"服务端能主动让用户下线"两大问题。

方案 A:Token + Redis(适合前后端分离 / App)

typescript
import { randomUUID } from 'crypto';

// 登录:生成 token,把用户信息存 Redis
async function login(user: { id: number; name: string }) {
  const token = randomUUID();
  await redis.set(
    `session:${token}`,
    JSON.stringify(user),
    'EX', 60 * 60 * 24 * 7 // 7天有效期
  );
  return token; // 返回给前端,前端存 localStorage / cookie
}

// 鉴权中间件:每个需要登录的接口都先过这里
async function authMiddleware(req, res, next) {
  const token = req.headers.authorization?.replace('Bearer ', '');
  if (!token) return res.status(401).json({ msg: '未登录' });

  const raw = await redis.get(`session:${token}`);
  if (!raw) return res.status(401).json({ msg: '登录已过期' });

  req.user = JSON.parse(raw);

  // 可选:滑动续期,用户活跃就刷新过期时间
  await redis.expire(`session:${token}`, 60 * 60 * 24 * 7);
  next();
}

// 退出登录:直接删 key,立刻失效(这是 JWT 做不到的)
async function logout(token: string) {
  await redis.del(`session:${token}`);
}

方案 B:express-session + connect-redis(适合传统 session)

typescript
import session from 'express-session';
import { RedisStore } from 'connect-redis';

app.use(session({
  store: new RedisStore({ client: redis }),
  secret: process.env.SESSION_SECRET!,
  resave: false,
  saveUninitialized: false,
  cookie: { maxAge: 1000 * 60 * 60 * 24 }, // 1天
}));

为什么不直接用纯 JWT? 纯 JWT 是无状态的,签发后服务端无法主动作废(改密码、踢人下线都做不到)。Redis 存登录态让你随时能"删 key 即下线",这是全栈项目里很关键的能力。

四、实战场景二:接口限流

实战二:用 Redis 做接口限流

防刷接口、防短信轰炸、防恶意请求,都靠限流。Redis 的原子自增 + 过期天生适合做这件事。

固定窗口限流(最简单,够大多数场景用)

typescript
/**
 * 限流:在 window 秒内,最多允许 limit 次
 * @returns true=放行  false=超限拦截
 */
async function rateLimit(key: string, limit: number, window: number): Promise<boolean> {
  const count = await redis.incr(key);     // 自增,原子操作
  if (count === 1) {
    await redis.expire(key, window);       // 第一次访问时设置窗口过期
  }
  return count <= limit;
}

// 用作中间件:每个 IP 每分钟最多 60 次
app.use(async (req, res, next) => {
  const key = `rate:${req.ip}`;
  const allowed = await rateLimit(key, 60, 60);
  if (!allowed) {
    return res.status(429).json({ msg: '请求太频繁,请稍后再试' });
  }
  next();
});

// 典型业务限流:发短信验证码,同一手机号 60 秒只能发一次
async function sendSms(phone: string) {
  const allowed = await rateLimit(`sms:${phone}`, 1, 60);
  if (!allowed) throw new Error('验证码发送过于频繁');
  // ... 真正发短信
}

第四坑(并发安全):固定窗口在高并发下 increxpire 两步之间有极小概率出问题。要更严谨可以用 Lua 脚本把判断+自增做成原子操作,或直接用现成库 rate-limiter-flexible(专为 Node 设计,支持 Redis):

typescript
import { RateLimiterRedis } from 'rate-limiter-flexible';
const limiter = new RateLimiterRedis({ storeClient: redis, points: 60, duration: 60 });
// limiter.consume(req.ip) 超限会 reject

前端配合:限流触发返回 429,前端应捕获并提示"操作太频繁",而不是直接报错崩页。

五、实战场景三:消息队列(异步任务)

实战三:用 Redis 做消息队列

发邮件、生成报表、处理图片、推送通知——这些耗时但不需要让用户等的活儿,应该丢进队列异步处理。接口立刻返回,后台慢慢消费。

入门版:用 List 自己撸一个

typescript
// 生产者:把任务塞进队列(接口里调用,立即返回)
async function addEmailJob(job: { to: string; subject: string }) {
  await redis.lpush('queue:email', JSON.stringify(job));
}

// 消费者:单独的 worker 进程,阻塞式取任务
async function emailWorker() {
  while (true) {
    // brpop 阻塞等待,有任务才返回,不空转浪费 CPU
    const res = await redis.brpop('queue:email', 0);
    if (res) {
      const job = JSON.parse(res[1]);
      await sendEmail(job); // 真正干活
    }
  }
}

生产版:直接上 BullMQ(强烈推荐)

自己撸的队列没有重试、没有失败处理、没有延迟任务。生产环境直接用 BullMQ(基于 Redis、专为 Node 打造、TS 友好):

bash
npm install bullmq
typescript
import { Queue, Worker } from 'bullmq';

const connection = { host: '127.0.0.1', port: 6379 };

// 1. 定义队列,生产者往里加任务
const emailQueue = new Queue('email', { connection });

app.post('/api/register', async (req, res) => {
  await db.user.create(req.body);
  // 注册成功,发欢迎邮件这种事丢队列,不阻塞响应
  await emailQueue.add('welcome', { to: req.body.email }, {
    attempts: 3,                              // 失败自动重试3次
    backoff: { type: 'exponential', delay: 2000 }, // 指数退避
  });
  res.json({ msg: '注册成功' }); // 立即返回,用户不用等邮件发完
});

// 2. 定义 worker 消费(通常单独进程跑)
new Worker('email', async (job) => {
  await sendEmail(job.data);
}, { connection });

为什么不一开始就用 BullMQ? 理解 List 版能让你明白队列的本质;但真实项目别重复造轮子,BullMQ 的重试、延迟任务、并发控制、可视化面板都是自己撸很难做好的。

六、实战场景四:实时状态同步(Pub/Sub)

实战四:用 Redis 做实时状态同步

在线状态、实时通知、多端数据同步这类需求,Redis 的**发布/订阅(Pub/Sub)**配合 WebSocket 是经典组合。

它解决的核心痛点是:当你的 Socket 服务多实例部署时,用户 A 连在实例 1、用户 B 连在实例 2,A 发的消息怎么推给 B? 答案就是用 Redis Pub/Sub 在实例之间广播。

typescript
// 注意:订阅和发布必须用两个独立的 Redis 连接
// 因为一个连接进入 subscribe 模式后就不能再执行普通命令了
const pub = new Redis();
const sub = new Redis();

// 某个实例:用户发了消息,广播给所有实例
async function broadcastMessage(msg: object) {
  await pub.publish('chat', JSON.stringify(msg));
}

// 所有实例:订阅频道,收到后推给自己连着的 WebSocket 客户端
sub.subscribe('chat');
sub.on('message', (channel, message) => {
  const data = JSON.parse(message);
  // io 是你的 socket.io 实例,把消息推给本实例下所有在线客户端
  io.emit('newMessage', data);
});

在线状态同步可以结合 Set + 过期:

typescript
// 用户上线(用带 TTL 的方式,心跳续期,断线自动过期下线)
async function setOnline(userId: string) {
  await redis.set(`online:${userId}`, '1', 'EX', 30); // 30秒无心跳即视为离线
}
// 前端每 20 秒发一次心跳调用 setOnline 续期

// 查某用户是否在线
async function isOnline(userId: string) {
  return (await redis.exists(`online:${userId}`)) === 1;
}

第五坑Pub/Sub 不保证可靠投递——订阅者掉线期间发布的消息收不到(不会补发)。如果你的消息绝对不能丢(如订单状态),别用 Pub/Sub,改用 Redis Stream 或正经消息队列。Pub/Sub 适合"丢了也无所谓"的实时推送(如弹幕、在线人数)。

七、JS 全栈用 Redis 的踩坑总清单

JS 全栈用 Redis 的常见踩坑点

把全文的坑汇总,写代码前对照检查:

坑点后果正确做法
每次请求 new Redis()连接数爆炸、服务崩全项目复用一个单例连接
存对象不序列化取出来是 [object Object]JSON.stringify,取 JSON.parse
忘设过期时间 TTL内存泄漏、脏数据缓存类 set 必带 EX
缓存穿透查不存在的 key 每次都打到数据库给"空结果"也缓存一个短 TTL 的空值
缓存雪崩大量 key 同时过期,DB 被打垮TTL 加随机值,错开过期时间
限流非原子操作高并发下限流不准用 Lua 脚本或 rate-limiter-flexible
Pub/Sub 当可靠队列用订阅者掉线丢消息可靠场景用 BullMQ / Redis Stream
sub 连接执行普通命令报错订阅和发布用两个独立连接
大 key / 一次存超大数据阻塞 Redis、拖慢所有请求拆分数据,避免单 key 过大
缓存与数据库不一致用户看到旧数据更新时删缓存(而非改),下次读回填

学习路径总结

按这个顺序推进,每一步都能直接用在项目里:

  1. 入门:装 ioredis,跑通连接,练熟 String / Hash / List 和 JSON.stringify 存取、TTL;
  2. 进阶:封装一个 cacheGet 旁路缓存工具,给你项目里最慢的那个查询接口加上缓存,亲眼看 QPS 提升;
  3. 实战:按"会话缓存 → 接口限流 → 消息队列(BullMQ) → 实时同步(Pub/Sub)"的顺序,一个场景一个场景落地;
  4. 避坑:把第七节清单贴在工位,写一行查一行。