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 redis3.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 World4.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: 24.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: 24.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: 25. 缓存策略
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.rdb6.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 64mb7. 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}`);
});代码解析:
项目结构:使用了模块化的项目结构,将代码分为配置、中间件、路由和控制器等部分
Redis 连接:使用 node-redis 的 Promise API,配置了自动重连和连接事件监听
会话管理:实现了会话的创建、获取、刷新和销毁功能,使用 Redis 存储会话数据
认证系统:实现了用户登录、登出和获取当前用户信息的功能
权限控制:实现了基于角色的权限控制,限制不同角色的访问权限
错误处理:对各种操作可能出现的错误进行了处理,返回友好的错误信息
中间件:使用了会话管理中间件,自动处理会话的验证和刷新
运行方法:
- 安装依赖:
npm install express redis dotenv uuid - 创建项目结构,将上述代码保存到对应文件
- 创建
.env文件,配置 Redis 连接信息 - 启动 Redis 服务
- 启动应用:
node app.js - 使用 API 测试工具(如 Postman)测试各个接口
学习目标
通过本集的学习,你应该能够:
- 理解 Redis 的基本概念和特点
- 掌握使用 node-redis 客户端连接 Redis 的方法
- 学会使用 Redis 的各种数据类型和操作命令
- 理解并应用常见的缓存策略,如缓存穿透、缓存击穿和缓存雪崩的解决方案
- 掌握 Redis 持久化的配置和使用方法
- 实现一个完整的用户会话管理系统,包括会话的创建、验证和销毁
- 应用 Redis 缓存提高应用性能和可靠性
小结
Redis 是一种功能强大、性能优异的内存数据库,广泛应用于缓存、会话管理、消息队列等场景。在 Node.js 应用中,使用 node-redis 客户端可以方便地与 Redis 服务器交互,实现各种高级功能。
通过本集的学习,你已经掌握了 Redis 的基本概念、node-redis 客户端的使用方法,以及如何应用各种缓存策略。你还实现了一个完整的用户会话管理系统,展示了 Redis 在实际应用中的价值。
在下一集中,我们将学习 Node.js 的部署和运维,了解如何将 Node.js 应用部署到生产环境,以及如何监控和维护应用的运行状态。