前端开发者的 Redis 入门指南
引言
作为前端开发者,你可能习惯了使用 localStorage 和 sessionStorage 来存储数据,但当你与后端协作时,常会听到"Redis"这个词。本文将从前端视角解释 Redis,帮助你快速理解这个强大的工具。
Redis 是什么?
Redis 是一个运行在服务器端的高性能键值存储数据库,主要将数据存储在内存中,可选择持久化到硬盘。它相当于服务器上的一个超级强化版的 localStorage。
localStorage 与 Redis 对比
| 特性 | localStorage | Redis |
|---|---|---|
| 运行环境 | 浏览器端 | 服务器端 |
| 存储容量 | 通常 5MB 左右 | 可达数 GB |
| 过期机制 | 无原生支持 | 内置过期时间设置 |
| 数据类型 | 仅字符串 | 字符串、哈希表、列表、集合等 |
| 作用域 | 仅限当前域名 | 所有后端服务可访问 |
| 持久性 | 持久存储直到手动清除 | 可配置持久化策略 |
| 访问方式 | 直接 JavaScript 访问 | 通过后端 API 间接访问 |
| 并发处理 | 不支持 | 支持原子操作、事务等 |
Redis 核心数据类型
注意 使用 ioredis 操作 Redis 时,设置过期时间时,需要使用"EX"参数。
字符串(String)
类似 localStorage 的键值存储:
// localStorage
localStorage.setItem("username", "zhangsan");
// Redis等效操作
redis.set("username", "zhangsan");
redis.set("username", "zhangsan", 3600); // 设置1小时过期
// 正确的ioredis写法
redis.set("username", "zhangsan", "EX", 3600);哈希表(Hash)
相当于存储 JavaScript 对象:
// localStorage需要序列化对象
localStorage.setItem("user", JSON.stringify({ name: "张三", age: 25 }));
// Redis直接支持对象字段操作
redis.hset("user:1001", "name", "张三");
redis.hset("user:1001", "age", "25");在 Redis 中形成的数据结构如下:
键: "user:1001" (示例)
值: {
name: "张三",
age: "25"
}列表(List)
适合存储有序数据集合:
// localStorage需要序列化数组
localStorage.setItem("messages", JSON.stringify(["消息1", "消息2"]));
// Redis列表操作
redis.rpush("messages", "消息1"); // 右侧添加
redis.lpush("messages", "新消息"); // 左侧添加
redis.lrange("messages", 0, -1); // 获取全部集合(Set)
存储不重复元素:
// localStorage无直接支持
const tags = new Set(["前端", "后端"]);
localStorage.setItem("tags", JSON.stringify([...tags]));
// Redis集合操作
redis.sadd("tags", "前端", "后端");
redis.sadd("tags", "前端"); // 重复添加无效前端如何与 Redis 交互?
前端不能直接访问 Redis,必须通过后端 API 间接操作:
// 前端请求
async function getUserProfile(userId) {
const response = await fetch(`/api/users/${userId}`);
return response.json();
}
// 后端实现(伪代码)
router.get("/api/users/:id", async (ctx) => {
// 先尝试从Redis获取
const cachedUser = await redisRepository.hgetall(`user:${ctx.params.id}`);
if (Object.keys(cachedUser).length > 0) {
return (ctx.body = cachedUser); // 返回缓存数据
}
// 缓存未命中,从数据库查询
const user = await database.getUser(ctx.params.id);
// 将结果存入Redis
await redisRepository.hset(`user:${ctx.params.id}`, "name", user.name);
await redisRepository.hset(
`user:${ctx.params.id}`,
"age",
user.age.toString()
);
await redisRepository.expire(`user:${ctx.params.id}`, 3600); // 1小时过期
return (ctx.body = user);
});Redis 的实际应用场景
1. 会话管理
替代传统的服务器会话存储,更适合分布式系统:
// 登录成功后,后端生成token并存储在Redis
redis.set(`session:${userId}`, sessionData, 86400); // 24小时过期
// 正确的ioredis写法
redis.set(`session:${userId}`, sessionData, "EX", 86400);
// 前端存储token
localStorage.setItem("token", receivedToken);2. 接口缓存
减少数据库查询,提高响应速度:
// 后端缓存热门数据
redis.set("hot_articles", JSON.stringify(articles), 600); // 10分钟过期
// 正确的ioredis写法
redis.set("hot_articles", JSON.stringify(articles), "EX", 600);3. 限流控制
防止恶意请求,保护 API 安全:
// 记录用户请求次数
redis.incr(`rate:${userId}`);
redis.expire(`rate:${userId}`, 60); // 60秒过期4. 实时数据
结合 WebSocket 提供实时数据更新:
// 发布消息到Redis频道
redis.publish("notifications", JSON.stringify(newNotification));
// WebSocket服务器订阅并推送给前端
redisClient.subscribe("notifications");数据库缓存模式详解
缓存存储格式选择
示例中使用的是哈希表(Hash)结构来缓存数据库查询结果:
// 将用户数据缓存为哈希表结构
await redisRepository.hset(`user:${userId}`, "name", "张三");
await redisRepository.hset(`user:${userId}`, "age", "25");在 Redis 中形成的数据结构如下:
键: "user:1001" (示例)
值: {
name: "张三",
age: "25"
}为什么选择哈希表而不是简单字符串?主要有三个原因:
字段级访问 - 可以单独获取或更新用户的某个属性(如
hget user:1001 name)内存效率 - 相比 JSON 字符串,哈希表在 Redis 内存管理上更高效
操作便捷 - 可以使用
hgetall一次获取完整对象,同时用单个expire指令设置整个对象过期
批量设置哈希表数据
在实际应用中,通常需要一次性将数据库查询结果存入 Redis 缓存。上面的示例是逐个字段设置,效率较低:
// 逐个设置字段(效率低)
await redisRepository.hset(`user:${id}`, "name", user.name);
await redisRepository.hset(`user:${id}`, "age", user.age.toString());Redis 提供了hmset命令,可以一次设置多个字段,大大提高性能:
// 批量设置多个字段(更高效)
await redisRepository.hmset(`user:${id}`, {
name: user.name,
age: user.age.toString(),
email: user.email,
role: user.role,
});
// 设置过期时间(单独操作)
await redisRepository.expire(`user:${id}`, 3600); // 1小时过期数据库对象直接缓存
初学者可能会想这样直接存储数据库返回的对象:
// 看似方便,实际有问题
await redisRepository.hmset(`user:${user.id}`, user);这种做法存在几个问题:
类型问题 - Redis 哈希表只能存储字符串值,而数据库对象可能包含数字、布尔值、日期等多种类型
复杂对象 - 嵌套对象、数组等无法直接存储,需要序列化
特殊字段 - 某些数据库返回的元数据字段可能不需要缓存
最佳实践:类型转换函数
推荐创建一个辅助函数处理对象转换:
// 准备对象用于Redis缓存的工具函数
function prepareForCache(obj) {
const result = {};
Object.entries(obj).forEach(([key, value]) => {
if (value !== null && value !== undefined) {
if (typeof value === "object") {
// 对象和数组需要序列化
result[key] = JSON.stringify(value);
} else {
// 其他类型转为字符串
result[key] = String(value);
}
}
});
return result;
}
// 使用
const user = await database.getUser(id);
await redisRepository.hmset(`user:${user.id}`, prepareForCache(user));
await redisRepository.expire(`user:${user.id}`, 3600);这种方式既保留了批量设置的效率优势,又解决了类型转换问题,是缓存数据库查询结果的推荐做法。
其他可选存储格式
除了哈希表,常见的 Redis 缓存格式还有:
1. JSON 字符串
// 将整个对象序列化为JSON字符串存储
redis.set("user:1001", JSON.stringify({ name: "张三", age: 25 }), 3600);
// 正确的ioredis写法
redis.set("user:1001", JSON.stringify({ name: "张三", age: 25 }), "EX", 3600);- 优点:简单直接,与 localStorage 使用方式类似
- 缺点:无法单独更新对象的某个字段,需要完整读取和写入
2. 分字段存储
// 每个字段单独存储
redis.set("user:1001:name", "张三", 3600);
redis.set("user:1001:age", "25", 3600);
// 正确的ioredis写法
redis.set("user:1001:name", "张三", "EX", 3600);
redis.set("user:1001:age", "25", "EX", 3600);优点:字段完全独立,可以设置不同过期时间
缺点:管理复杂,需要维护多个键的关联
缓存更新策略
Redis 缓存通常采用以下策略:
缓存穿透 - 对不存在的数据也缓存 null 值,防止频繁查询数据库
缓存击穿 - 对热点数据使用互斥锁或提前更新机制
缓存雪崩 - 设置随机过期时间,避免同时失效
// 缓存穿透处理示例
if (cachedUser === null) {
// 明确缓存了null
return (ctx.body = { error: "用户不存在" });
} else if (cachedUser === undefined) {
// 未缓存
const user = await database.getUser(id);
if (!user) {
// 缓存null值,避免反复查询
await redis.set(`user:${id}`, "null", 300); // 短期缓存5分钟
// 正确的ioredis写法
// await redis.set(`user:${id}`, "null", "EX", 300);
return (ctx.body = { error: "用户不存在" });
}
// 正常缓存数据...
}总结
Redis 作为服务器端的高性能数据存储解决方案,远比 localStorage 强大。对前端开发者而言,了解 Redis 有助于:
更好地理解后端数据缓存机制
与后端开发者进行更有效的沟通
设计更高效的前后端交互方案
为全栈开发打下基础
虽然你可能不会直接操作 Redis,但理解其工作原理和使用场景,将帮助你成为更全面的开发者。