Node.js Redis 缓存

章节概述

Redis 是一种流行的内存数据库,广泛应用于缓存、会话管理、消息队列等场景。在 Node.js 应用中,我们通常使用 node-redis 客户端来与 Redis 服务器交互,它提供了高效、可靠的 Redis 操作接口。本集将详细介绍如何在 Node.js 中使用 Redis 进行缓存,实现用户会话管理,以及应用各种缓存策略。

核心知识点讲解

1. Redis 简介

Redis(Remote Dictionary Server)是一个开源的、基于内存的键值对存储数据库,它的主要特点包括:

  • 内存存储:数据存储在内存中,读写速度极快
  • 持久化:支持将内存数据持久化到磁盘,保证数据安全
  • 丰富的数据类型:支持字符串、哈希、列表、集合、有序集合等多种数据类型
  • 高并发:单线程模型,避免了线程切换开销,支持数万 QPS
  • 可扩展性:支持主从复制、哨兵模式和集群模式
  • 丰富的功能:支持事务、发布/订阅、Lua 脚本等高级功能
  • 跨平台:可在多种操作系统上运行

2. node-redis 客户端简介

node-redis 是 Node.js 的官方 Redis 客户端,提供了与 Redis 服务器交互的完整 API:

  • Promise 支持:原生支持 Promise API,便于使用 async/await
  • 自动重连:当连接断开时,自动尝试重连
  • 管道:支持命令管道,提高批量操作性能
  • 事务:支持 Redis 事务
  • 发布/订阅:支持 Redis 的发布/订阅功能
  • Lua 脚本:支持执行 Lua 脚本
  • 连接池:支持连接池,优化连接管理

3. 连接 Redis

首先,需要安装 redis 模块:

npm install redis

3.1 基本连接

const { createClient } = require('redis');

// 创建 Redis 客户端
const client = createClient({
  url: 'redis://localhost:6379' // Redis 服务器地址
});

// 连接 Redis
async function connect() {
  try {
    await client.connect();
    console.log('成功连接到 Redis 服务器');
  } catch (error) {
    console.error('连接 Redis 失败:', error);
  }
}

// 调用连接函数
connect();

// 关闭连接
// await client.disconnect();

3.2 连接配置

const { createClient } = require('redis');

// 创建 Redis 客户端
const client = createClient({
  url: 'redis://localhost:6379',
  socket: {
    reconnectStrategy: (retries) => {
      // 重连策略:指数退避
      return Math.min(retries * 100, 3000);
    },
    connectTimeout: 10000, // 连接超时时间(毫秒)
  },
  password: 'your-password', // Redis 密码(如果有)
  database: 0, // 数据库索引(默认 0)
});

// 监听连接事件
client.on('connect', () => {
  console.log('正在连接到 Redis 服务器');
});

client.on('ready', () => {
  console.log('Redis 连接已准备就绪');
});

client.on('error', (error) => {
  console.error('Redis 连接错误:', error);
});

client.on('end', () => {
  console.log('Redis 连接已关闭');
});

// 连接 Redis
async function connect() {
  try {
    await client.connect();
  } catch (error) {
    console.error('连接 Redis 失败:', error);
  }
}

connect();

4. Redis 数据类型和操作

4.1 字符串(String)

字符串是 Redis 最基本的数据类型,用于存储文本或二进制数据。

// 设置字符串
await client.set('name', 'John Doe');

// 获取字符串
const name = await client.get('name');
console.log('Name:', name); // 输出: Name: John Doe

// 设置带过期时间的字符串(10 秒后过期)
await client.set('token', 'abc123', { EX: 10 });

// 自增
await client.set('counter', 0);
await client.incr('counter'); // 变为 1
await client.incrBy('counter', 5); // 变为 6

// 自减
await client.decr('counter'); // 变为 5
await client.decrBy('counter', 2); // 变为 3

// 追加字符串
await client.set('message', 'Hello');
await client.append('message', ' World');
const message = await client.get('message');
console.log('Message:', message); // 输出: Message: Hello World

4.2 哈希(Hash)

哈希用于存储对象,类似于 JavaScript 中的对象。

// 设置哈希字段
await client.hSet('user:1', {
  name: 'John Doe',
  email: 'john@example.com',
  age: 30
});

// 获取哈希字段
const name = await client.hGet('user:1', 'name');
console.log('Name:', name); // 输出: Name: John Doe

// 获取所有哈希字段
const user = await client.hGetAll('user:1');
console.log('User:', user); // 输出: User: { name: 'John Doe', email: 'john@example.com', age: '30' }

// 检查哈希字段是否存在
const exists = await client.hExists('user:1', 'name');
console.log('Name exists:', exists); // 输出: Name exists: true

// 删除哈希字段
await client.hDel('user:1', 'age');

// 获取哈希字段数量
const count = await client.hLen('user:1');
console.log('Field count:', count); // 输出: Field count: 2

4.3 列表(List)

列表是一个有序的字符串集合,支持在两端进行操作。

// 在列表左侧添加元素
await client.lPush('tasks', 'Task 1');
await client.lPush('tasks', 'Task 2');
await client.lPush('tasks', 'Task 3');

// 在列表右侧添加元素
await client.rPush('tasks', 'Task 4');

// 获取列表元素
const tasks = await client.lRange('tasks', 0, -1);
console.log('Tasks:', tasks); // 输出: Tasks: [ 'Task 3', 'Task 2', 'Task 1', 'Task 4' ]

// 从列表左侧移除元素
const task = await client.lPop('tasks');
console.log('Popped task:', task); // 输出: Popped task: Task 3

// 从列表右侧移除元素
const task = await client.rPop('tasks');
console.log('Popped task:', task); // 输出: Popped task: Task 4

// 获取列表长度
const length = await client.lLen('tasks');
console.log('Task count:', length); // 输出: Task count: 2

4.4 集合(Set)

集合是一个无序的、唯一的字符串集合。

// 添加集合元素
await client.sAdd('tags', 'nodejs');
await client.sAdd('tags', 'javascript');
await client.sAdd('tags', 'redis');
await client.sAdd('tags', 'nodejs'); // 重复元素,会被忽略

// 获取集合所有元素
const tags = await client.sMembers('tags');
console.log('Tags:', tags); // 输出: Tags: [ 'nodejs', 'javascript', 'redis' ]

// 检查元素是否在集合中
const exists = await client.sIsMember('tags', 'nodejs');
console.log('Node.js exists:', exists); // 输出: Node.js exists: true

// 删除集合元素
await client.sRem('tags', 'javascript');

// 获取集合大小
const size = await client.sCard('tags');
console.log('Tag count:', size); // 输出: Tag count: 2

// 集合交集
await client.sAdd('tags1', 'a', 'b', 'c');
await client.sAdd('tags2', 'b', 'c', 'd');
const intersection = await client.sInter('tags1', 'tags2');
console.log('Intersection:', intersection); // 输出: Intersection: [ 'b', 'c' ]

// 集合并集
const union = await client.sUnion('tags1', 'tags2');
console.log('Union:', union); // 输出: Union: [ 'a', 'b', 'c', 'd' ]

4.5 有序集合(Sorted Set)

有序集合是一个有序的、唯一的字符串集合,每个元素都有一个分数,用于排序。

// 添加有序集合元素
await client.zAdd('scores', {
  score: 90, 
  value: 'Alice'
});
await client.zAdd('scores', {
  score: 85, 
  value: 'Bob'
});
await client.zAdd('scores', {
  score: 95, 
  value: 'Charlie'
});

// 获取有序集合元素(按分数从低到高)
const scoresAsc = await client.zRange('scores', 0, -1, { WITHSCORES: true });
console.log('Scores (asc):', scoresAsc); // 输出: Scores (asc): [ 'Bob', '85', 'Alice', '90', 'Charlie', '95' ]

// 获取有序集合元素(按分数从高到低)
const scoresDesc = await client.zRevRange('scores', 0, -1, { WITHSCORES: true });
console.log('Scores (desc):', scoresDesc); // 输出: Scores (desc): [ 'Charlie', '95', 'Alice', '90', 'Bob', '85' ]

// 根据分数范围获取元素
const range = await client.zRangeByScore('scores', 80, 90, { WITHSCORES: true });
console.log('Scores (80-90):', range); // 输出: Scores (80-90): [ 'Bob', '85', 'Alice', '90' ]

// 获取元素分数
const aliceScore = await client.zScore('scores', 'Alice');
console.log('Alice\'s score:', aliceScore); // 输出: Alice's score: 90

// 删除有序集合元素
await client.zRem('scores', 'Bob');

// 获取有序集合大小
const size = await client.zCard('scores');
console.log('Score count:', size); // 输出: Score count: 2

5. 缓存策略

5.1 缓存穿透

缓存穿透是指查询一个不存在的数据,导致请求直接打到数据库,增加数据库负担。

解决方案:

  • 布隆过滤器:在缓存前添加布隆过滤器,快速判断数据是否存在
  • 空值缓存:对于不存在的数据,也在缓存中存储一个空值,设置较短的过期时间
  • 参数校验:对输入参数进行校验,过滤明显不存在的请求
// 空值缓存示例
async function getUser(id) {
  // 尝试从缓存获取
  const cachedUser = await client.get(`user:${id}`);
  if (cachedUser) {
    if (cachedUser === 'null') {
      return null; // 空值缓存
    }
    return JSON.parse(cachedUser);
  }
  
  // 从数据库获取
  const user = await db.query('SELECT * FROM users WHERE id = ?', [id]);
  
  // 存储到缓存
  if (user) {
    await client.set(`user:${id}`, JSON.stringify(user), { EX: 3600 });
  } else {
    await client.set(`user:${id}`, 'null', { EX: 60 }); // 空值缓存,过期时间较短
  }
  
  return user;
}

5.2 缓存击穿

缓存击穿是指一个热点 key 过期后,大量请求同时打到数据库,导致数据库压力骤增。

解决方案:

  • 互斥锁:当缓存过期时,只允许一个请求去数据库查询,其他请求等待
  • 热点数据永不过期:对于热点数据,不设置过期时间,由后台任务定期更新
  • 预加载:在缓存过期前,主动更新缓存
// 互斥锁示例
async function getUserWithLock(id) {
  // 尝试从缓存获取
  let cachedUser = await client.get(`user:${id}`);
  if (cachedUser) {
    return JSON.parse(cachedUser);
  }
  
  // 尝试获取锁
  const lockKey = `lock:user:${id}`;
  const lockAcquired = await client.set(lockKey, '1', { NX: true, EX: 5 });
  
  if (lockAcquired) {
    try {
      // 从数据库获取
      const user = await db.query('SELECT * FROM users WHERE id = ?', [id]);
      
      // 存储到缓存
      if (user) {
        await client.set(`user:${id}`, JSON.stringify(user), { EX: 3600 });
      }
      
      return user;
    } finally {
      // 释放锁
      await client.del(lockKey);
    }
  } else {
    // 锁被其他请求获取,等待后重试
    await new Promise(resolve => setTimeout(resolve, 100));
    return getUserWithLock(id);
  }
}

5.3 缓存雪崩

缓存雪崩是指大量缓存同时过期,导致请求全部打到数据库,造成数据库崩溃。

解决方案:

  • 随机过期时间:为缓存设置随机的过期时间,避免同时过期
  • 分层缓存:使用多级缓存,不同层级的缓存设置不同的过期时间
  • 缓存预热:在系统启动时,主动加载热点数据到缓存
  • 降级方案:当缓存失效时,提供降级服务,避免直接打到数据库
// 随机过期时间示例
async function setCacheWithRandomExpiry(key, value) {
  // 基础过期时间 1 小时
  const baseExpiry = 3600;
  // 随机添加 0-30 分钟
  const randomExpiry = Math.floor(Math.random() * 1800);
  const totalExpiry = baseExpiry + randomExpiry;
  
  await client.set(key, value, { EX: totalExpiry });
}

6. Redis 持久化

Redis 提供了两种持久化方式,用于将内存数据保存到磁盘:

6.1 RDB(Redis Database)

RDB 是一种快照持久化方式,定期将内存中的数据快照保存到磁盘:

  • 优点:文件体积小,恢复速度快
  • 缺点:可能会丢失最近的写操作

配置示例:

# 900秒内如果至少有1个key被修改,则执行快照
save 900 1
# 300秒内如果至少有10个key被修改,则执行快照
save 300 10
# 60秒内如果至少有10000个key被修改,则执行快照
save 60 10000

# 快照文件路径
dir ./
# 快照文件名
dbfilename dump.rdb

6.2 AOF(Append Only File)

AOF 是一种日志持久化方式,将所有写操作追加到日志文件:

  • 优点:数据安全性高,不会丢失数据
  • 缺点:文件体积大,恢复速度慢

配置示例:

# 开启 AOF 持久化
appendonly yes

# AOF 文件路径
appendfilename "appendonly.aof"

# 同步策略:always(每次写操作都同步)、everysec(每秒同步)、no(由操作系统决定)
appendfsync everysec

# 重写策略:当 AOF 文件大小超过上次重写后的 100%,且大于 64MB 时触发重写
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

7. Redis 连接池

对于高并发应用,使用连接池管理 Redis 连接可以提高性能:

const { createClient } = require('redis');

// 创建 Redis 客户端(自动管理连接池)
const client = createClient({
  url: 'redis://localhost:6379',
  socket: {
    poolSize: 10, // 连接池大小
  },
});

// 连接 Redis
async function connect() {
  try {
    await client.connect();
    console.log('成功连接到 Redis 服务器');
  } catch (error) {
    console.error('连接 Redis 失败:', error);
  }
}

connect();

// 执行命令(自动使用连接池中的连接)
async function runCommands() {
  for (let i = 0; i < 1000; i++) {
    await client.set(`key:${i}`, `value:${i}`);
    const value = await client.get(`key:${i}`);
    console.log(`key:${i} = ${value}`);
  }
}

runCommands();

实用案例分析

案例:用户会话管理系统

下面我们将使用 Express 和 redis 实现一个完整的用户会话管理系统,支持用户登录、会话验证和会话管理。

项目结构

session-management-system/
├── app.js
├── config/
│   └── redis.js
├── middleware/
│   └── session.js
├── routes/
│   ├── auth.js
│   └── user.js
├── controllers/
│   ├── authController.js
│   └── userController.js
├── utils/
│   └── token.js
├── package.json
└── .env

安装依赖

npm install express redis dotenv uuid

配置文件(.env)

# Redis 配置
REDIS_URL=redis://localhost:6379

# 服务器端口
PORT=3000

# 会话配置
SESSION_EXPIRY=86400 # 会话过期时间(秒)

Redis 配置(config/redis.js)

const { createClient } = require('redis');
require('dotenv').config();

// 创建 Redis 客户端
const client = createClient({
  url: process.env.REDIS_URL,
  socket: {
    reconnectStrategy: (retries) => {
      return Math.min(retries * 100, 3000);
    },
    connectTimeout: 10000,
  },
});

// 监听连接事件
client.on('connect', () => {
  console.log('正在连接到 Redis 服务器');
});

client.on('ready', () => {
  console.log('Redis 连接已准备就绪');
});

client.on('error', (error) => {
  console.error('Redis 连接错误:', error);
});

client.on('end', () => {
  console.log('Redis 连接已关闭');
});

// 连接 Redis
async function connect() {
  try {
    await client.connect();
    console.log('成功连接到 Redis 服务器');
  } catch (error) {
    console.error('连接 Redis 失败:', error);
  }
}

// 导出客户端和连接函数
module.exports = {
  client,
  connect
};

会话管理中间件(middleware/session.js)

const { client } = require('../config/redis');
require('dotenv').config();

// 会话管理中间件
async function sessionMiddleware(req, res, next) {
  try {
    // 从请求头获取会话 ID
    const sessionId = req.headers['x-session-id'] || req.cookies?.sessionId;
    
    if (!sessionId) {
      req.session = null;
      return next();
    }
    
    // 从 Redis 获取会话数据
    const sessionData = await client.get(`session:${sessionId}`);
    
    if (!sessionData) {
      req.session = null;
      return next();
    }
    
    // 解析会话数据
    req.session = JSON.parse(sessionData);
    req.sessionId = sessionId;
    
    // 刷新会话过期时间
    await client.expire(`session:${sessionId}`, parseInt(process.env.SESSION_EXPIRY));
    
    next();
  } catch (error) {
    console.error('会话管理中间件错误:', error);
    req.session = null;
    next();
  }
}

// 创建会话
async function createSession(userId, userData) {
  const sessionId = `session:${require('uuid').v4()}`;
  const sessionData = {
    userId,
    ...userData,
    createdAt: Date.now(),
  };
  
  // 存储会话到 Redis
  await client.set(
    sessionId,
    JSON.stringify(sessionData),
    { EX: parseInt(process.env.SESSION_EXPIRY) }
  );
  
  return sessionId;
}

// 销毁会话
async function destroySession(sessionId) {
  await client.del(`session:${sessionId}`);
}

module.exports = {
  sessionMiddleware,
  createSession,
  destroySession
};

认证控制器(controllers/authController.js)

const { createSession, destroySession } = require('../middleware/session');

// 模拟用户数据库
const users = [
  { id: 1, username: 'admin', password: 'admin123', role: 'admin' },
  { id: 2, username: 'user', password: 'user123', role: 'user' }
];

// 用户登录
async function login(req, res) {
  try {
    const { username, password } = req.body;
    
    // 验证用户名和密码
    const user = users.find(u => u.username === username && u.password === password);
    if (!user) {
      return res.status(401).json({ success: false, message: '用户名或密码错误' });
    }
    
    // 创建会话
    const sessionId = await createSession(user.id, {
      username: user.username,
      role: user.role
    });
    
    // 返回会话 ID
    res.json({ 
      success: true, 
      message: '登录成功',
      data: {
        sessionId: sessionId.replace('session:', ''),
        user: {
          id: user.id,
          username: user.username,
          role: user.role
        }
      }
    });
  } catch (error) {
    console.error('登录失败:', error);
    res.status(500).json({ success: false, message: '登录失败', error: error.message });
  }
}

// 用户登出
async function logout(req, res) {
  try {
    const sessionId = req.sessionId;
    if (sessionId) {
      await destroySession(sessionId);
    }
    
    res.json({ success: true, message: '登出成功' });
  } catch (error) {
    console.error('登出失败:', error);
    res.status(500).json({ success: false, message: '登出失败', error: error.message });
  }
}

// 获取当前用户信息
async function getCurrentUser(req, res) {
  try {
    if (!req.session) {
      return res.status(401).json({ success: false, message: '未登录' });
    }
    
    res.json({ 
      success: true, 
      data: {
        user: {
          id: req.session.userId,
          username: req.session.username,
          role: req.session.role
        }
      }
    });
  } catch (error) {
    console.error('获取用户信息失败:', error);
    res.status(500).json({ success: false, message: '获取用户信息失败', error: error.message });
  }
}

module.exports = {
  login,
  logout,
  getCurrentUser
};

用户控制器(controllers/userController.js)

// 模拟用户数据
const users = [
  { id: 1, username: 'admin', role: 'admin' },
  { id: 2, username: 'user', role: 'user' },
  { id: 3, username: 'guest', role: 'guest' }
];

// 获取所有用户
async function getAllUsers(req, res) {
  try {
    // 检查权限
    if (!req.session || req.session.role !== 'admin') {
      return res.status(403).json({ success: false, message: '权限不足' });
    }
    
    res.json({ success: true, data: users });
  } catch (error) {
    console.error('获取用户列表失败:', error);
    res.status(500).json({ success: false, message: '获取用户列表失败', error: error.message });
  }
}

// 获取用户详情
async function getUserById(req, res) {
  try {
    // 检查登录状态
    if (!req.session) {
      return res.status(401).json({ success: false, message: '未登录' });
    }
    
    const { id } = req.params;
    const user = users.find(u => u.id === parseInt(id));
    
    if (!user) {
      return res.status(404).json({ success: false, message: '用户不存在' });
    }
    
    // 检查权限:只能查看自己的信息,或管理员可以查看所有
    if (req.session.role !== 'admin' && req.session.userId !== user.id) {
      return res.status(403).json({ success: false, message: '权限不足' });
    }
    
    res.json({ success: true, data: user });
  } catch (error) {
    console.error('获取用户详情失败:', error);
    res.status(500).json({ success: false, message: '获取用户详情失败', error: error.message });
  }
}

module.exports = {
  getAllUsers,
  getUserById
};

认证路由(routes/auth.js)

const express = require('express');
const router = express.Router();
const authController = require('../controllers/authController');
const { sessionMiddleware } = require('../middleware/session');

// 登录
router.post('/login', authController.login);

// 登出
router.post('/logout', sessionMiddleware, authController.logout);

// 获取当前用户信息
router.get('/me', sessionMiddleware, authController.getCurrentUser);

module.exports = router;

用户路由(routes/user.js)

const express = require('express');
const router = express.Router();
const userController = require('../controllers/userController');
const { sessionMiddleware } = require('../middleware/session');

// 获取所有用户
router.get('/', sessionMiddleware, userController.getAllUsers);

// 获取用户详情
router.get('/:id', sessionMiddleware, userController.getUserById);

module.exports = router;

主应用文件(app.js)

const express = require('express');
const dotenv = require('dotenv');
const { connect } = require('./config/redis');
const { sessionMiddleware } = require('./middleware/session');
const authRoutes = require('./routes/auth');
const userRoutes = require('./routes/user');

// 加载环境变量
dotenv.config();

// 创建 Express 应用
const app = express();

// 中间件
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// 会话管理中间件
app.use(sessionMiddleware);

// 路由
app.use('/api/auth', authRoutes);
app.use('/api/users', userRoutes);

// 健康检查
app.get('/health', (req, res) => {
  res.json({ status: 'ok', message: '服务运行正常' });
});

// 连接 Redis
connect();

// 启动服务器
const port = process.env.PORT || 3000;
app.listen(port, () => {
  console.log(`服务器已启动,监听端口 ${port}`);
});

代码解析:

  1. 项目结构:使用了模块化的项目结构,将代码分为配置、中间件、路由和控制器等部分

  2. Redis 连接:使用 node-redis 的 Promise API,配置了自动重连和连接事件监听

  3. 会话管理:实现了会话的创建、获取、刷新和销毁功能,使用 Redis 存储会话数据

  4. 认证系统:实现了用户登录、登出和获取当前用户信息的功能

  5. 权限控制:实现了基于角色的权限控制,限制不同角色的访问权限

  6. 错误处理:对各种操作可能出现的错误进行了处理,返回友好的错误信息

  7. 中间件:使用了会话管理中间件,自动处理会话的验证和刷新

运行方法:

  1. 安装依赖:npm install express redis dotenv uuid
  2. 创建项目结构,将上述代码保存到对应文件
  3. 创建 .env 文件,配置 Redis 连接信息
  4. 启动 Redis 服务
  5. 启动应用:node app.js
  6. 使用 API 测试工具(如 Postman)测试各个接口

学习目标

通过本集的学习,你应该能够:

  1. 理解 Redis 的基本概念和特点
  2. 掌握使用 node-redis 客户端连接 Redis 的方法
  3. 学会使用 Redis 的各种数据类型和操作命令
  4. 理解并应用常见的缓存策略,如缓存穿透、缓存击穿和缓存雪崩的解决方案
  5. 掌握 Redis 持久化的配置和使用方法
  6. 实现一个完整的用户会话管理系统,包括会话的创建、验证和销毁
  7. 应用 Redis 缓存提高应用性能和可靠性

小结

Redis 是一种功能强大、性能优异的内存数据库,广泛应用于缓存、会话管理、消息队列等场景。在 Node.js 应用中,使用 node-redis 客户端可以方便地与 Redis 服务器交互,实现各种高级功能。

通过本集的学习,你已经掌握了 Redis 的基本概念、node-redis 客户端的使用方法,以及如何应用各种缓存策略。你还实现了一个完整的用户会话管理系统,展示了 Redis 在实际应用中的价值。

在下一集中,我们将学习 Node.js 的部署和运维,了解如何将 Node.js 应用部署到生产环境,以及如何监控和维护应用的运行状态。

« 上一篇 Node.js MySQL 连接 下一篇 » Node.js 认证与授权