主题
Redis 实战
Redis 基础
什么是 Redis
Redis(Remote Dictionary Server)是一个开源的、内存中的数据结构存储系统,可用作数据库、缓存和消息代理。它支持多种数据结构,如字符串、哈希、列表、集合、有序集合等。
Redis 为什么快
┌───────────────────────────────────────────────────┐
│ Redis 性能优势 │
├───────────────────┬───────────────────────────────┤
│ 1. 内存存储 │ 数据全部在内存,读写速度快 │
│ 2. 单线程模型 │ 避免线程切换和锁竞争 │
│ 3. I/O 多路复用 │ 高效处理并发连接 │
│ 4. 精简数据结构 │ 底层实现优化,占用空间小 │
│ 5. 网络模型 │ 非阻塞 I/O,响应迅速 │
└───────────────────┴───────────────────────────────┘Redis 6.0 多线程
Redis 6.0 引入了多线程,但只用于处理网络 I/O,核心命令执行仍然是单线程的,这样既保持了单线程的简单性,又提高了并发处理能力。
五种基本数据结构
String(字符串)
用途:缓存、计数器、分布式锁
常用命令:
SET key value:设置键值对GET key:获取值INCR key:自增DECR key:自减EXPIRE key seconds:设置过期时间
示例:
javascript
// 计数器
await redisClient.incr('page_view');
// 缓存用户信息
await redisClient.set('user:1000', JSON.stringify({ id: 1000, name: 'John' }));
await redisClient.expire('user:1000', 3600);List(列表)
用途:消息队列、最新列表、栈/队列
常用命令:
LPUSH key value:从左侧插入RPUSH key value:从右侧插入LPOP key:从左侧弹出RPOP key:从右侧弹出LLEN key:获取列表长度LRANGE key start stop:获取指定范围的元素
示例:
javascript
// 消息队列
await redisClient.lpush('messages', 'hello');
const message = await redisClient.rpop('messages');
// 最新文章列表
await redisClient.lpush('latest_articles', 'article:1001');
await redisClient.ltrim('latest_articles', 0, 9); // 只保留最新 10 篇Hash(哈希)
用途:对象存储、用户信息
常用命令:
HSET key field value:设置字段值HGET key field:获取字段值HGETALL key:获取所有字段值HDEL key field:删除字段HMSET key field1 value1 field2 value2:设置多个字段值
示例:
javascript
// 存储用户信息
await redisClient.hset('user:1000', {
name: 'John',
email: 'john@example.com',
age: 30
});
const user = await redisClient.hgetall('user:1000');Set(集合)
用途:去重、交集并集差集
常用命令:
SADD key member:添加成员SREM key member:移除成员SMEMBERS key:获取所有成员SISMEMBER key member:判断成员是否存在SINTER key1 key2:交集SUNION key1 key2:并集SDIFF key1 key2:差集
示例:
javascript
// 标签管理
await redisClient.sadd('article:1001:tags', 'nodejs', 'redis', 'javascript');
// 用户关注
await redisClient.sadd('user:1000:following', 'user:1001', 'user:1002');
await redisClient.sadd('user:1001:followers', 'user:1000');
// 共同关注
const mutualFollows = await redisClient.sinter('user:1000:following', 'user:1001:following');Sorted Set(有序集合)
用途:排行榜、延迟队列、范围查询
常用命令:
ZADD key score member:添加成员及分数ZREM key member:移除成员ZRANGE key start stop:按分数升序获取成员ZREVRANGE key start stop:按分数降序获取成员ZSCORE key member:获取成员分数ZINCRBY key increment member:增加成员分数
示例:
javascript
// 排行榜
await redisClient.zadd('leaderboard', 100, 'user:1000');
await redisClient.zadd('leaderboard', 200, 'user:1001');
// 获取前 10 名
const top10 = await redisClient.zrevrange('leaderboard', 0, 9, 'WITHSCORES');
// 延迟队列
const score = Date.now() + 60000; // 1 分钟后
await redisClient.zadd('delayed_jobs', score, 'job:1001');
// 处理到期任务
const now = Date.now();
const jobs = await redisClient.zrangebyscore('delayed_jobs', 0, now);高级数据结构
HyperLogLog
用途:基数统计(如独立用户数)
特点:占用空间小,误差率约 0.81%
常用命令:
PFADD key element:添加元素PFCOUNT key:获取基数PFMERGE destkey sourcekey1 sourcekey2:合并多个 HyperLogLog
示例:
javascript
// 统计独立访客
await redisClient.pfadd('unique_visitors', 'user:1000', 'user:1001', 'user:1002');
const count = await redisClient.pfcount('unique_visitors');
console.log(`独立访客数:${count}`);Bitmap
用途:布尔值存储(如用户签到、在线状态)
常用命令:
SETBIT key offset value:设置位值GETBIT key offset:获取位值BITCOUNT key:统计 1 的个数BITOP operation destkey key1 key2:位运算
示例:
javascript
// 用户签到
const userId = 1000;
const dayOfYear = 100; // 一年中的第 100 天
await redisClient.setbit(`user:${userId}:checkins`, dayOfYear, 1);
// 统计签到天数
const checkinDays = await redisClient.bitcount(`user:${userId}:checkins`);Geo
用途:地理位置(如附近的人、商家)
常用命令:
GEOADD key longitude latitude member:添加位置GEODIST key member1 member2 unit:计算距离GEORADIUS key longitude latitude radius unit:获取指定范围内的成员
示例:
javascript
// 添加商家位置
await redisClient.geoadd('restaurants', 116.404, 39.915, 'restaurant:1001');
await redisClient.geoadd('restaurants', 116.414, 39.925, 'restaurant:1002');
// 查找附近的商家(5 公里内)
const nearbyRestaurants = await redisClient.georadius('restaurants', 116.404, 39.915, 5, 'km');Stream
用途:消息队列(支持消费组)
常用命令:
XADD key * field value:添加消息XREAD COUNT count BLOCK milliseconds STREAMS key id:读取消息XGROUP CREATE key groupname id:创建消费组XREADGROUP GROUP groupname consumername COUNT count BLOCK milliseconds STREAMS key >:消费组读取消息
示例:
javascript
// 生产消息
await redisClient.xadd('orders', '*', 'orderId', '1001', 'userId', '2001');
// 消费消息
const messages = await redisClient.xread('COUNT', 10, 'BLOCK', 0, 'STREAMS', 'orders', '0');缓存策略
缓存模式
| 模式 | 描述 | 优点 | 缺点 |
|---|---|---|---|
| Cache-Aside | 应用先查缓存,未命中则查数据库并更新缓存 | 简单,适用范围广 | 存在缓存不一致风险 |
| Read-Through | 缓存负责查询数据库并更新自己 | 逻辑清晰,一致性好 | 实现复杂 |
| Write-Through | 应用写缓存,缓存负责写数据库 | 一致性好 | 写性能受数据库影响 |
| Write-Back | 应用写缓存,缓存异步写数据库 | 写性能好 | 有数据丢失风险 |
缓存问题与解决方案
1. 缓存穿透
问题:请求不存在的数据,导致每次都查询数据库
解决方案:
- 布隆过滤器:过滤不存在的键
- 空值缓存:缓存不存在的数据,设置较短的过期时间
2. 缓存击穿
问题:热点数据过期,导致大量请求同时查询数据库
解决方案:
- 互斥锁:只允许一个线程更新缓存
- 逻辑过期:缓存不过期,通过后台线程更新
- 永不过期:热点数据永不自动过期
3. 缓存雪崩
问题:大量缓存同时过期,导致数据库压力骤增
解决方案:
- 随机过期时间:为缓存设置随机的过期时间
- 多级缓存:使用本地缓存 + 分布式缓存
- 限流降级:对数据库访问进行限流
分布式锁
基本实现
javascript
// 获取锁
async function acquireLock(key, value, expireTime) {
return await redisClient.set(key, value, 'NX', 'EX', expireTime);
}
// 释放锁(使用 Lua 脚本保证原子性)
async function releaseLock(key, value) {
const script = `
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
`;
return await redisClient.eval(script, 1, key, value);
}Redlock 算法
Redlock 是 Redis 官方推荐的分布式锁算法,适用于多个 Redis 实例的场景,提高了锁的可靠性。
步骤:
- 获取当前时间
- 依次向 N 个 Redis 实例获取锁
- 计算获取锁的时间
- 如果获取锁的时间小于过期时间,且成功获取了多数实例的锁,则锁获取成功
- 否则,释放所有实例的锁
Redis 持久化
RDB(Redis Database)
特点:
- 定期生成快照,将数据持久化到磁盘
- 恢复速度快
- 适合备份和灾难恢复
配置:
ini
save 900 1 # 900 秒内有 1 个键变化
save 300 10 # 300 秒内有 10 个键变化
save 60 10000 # 60 秒内有 10000 个键变化AOF(Append Only File)
特点:
- 记录所有写操作,追加到文件
- 数据更安全,丢失数据少
- 恢复速度较慢
配置:
ini
aof yes # 开启 AOF
aof-use-rdb-preamble yes # 混合持久化
appendfsync everysec # 每秒同步一次混合持久化
Redis 4.0 引入了混合持久化,结合了 RDB 和 AOF 的优点:
- 使用 RDB 作为基础,保证恢复速度
- 使用 AOF 记录增量操作,保证数据安全性
Redis 集群
主从复制
作用:
- 数据备份
- 负载均衡(读操作)
- 高可用性
配置:
ini
replicaof <masterip> <masterport> # 从节点配置哨兵模式(Sentinel)
作用:
- 监控 Redis 实例
- 自动故障转移
- 配置中心
配置:
ini
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 30000
sentinel failover-timeout mymaster 180000
sentinel parallel-syncs mymaster 1Redis Cluster
作用:
- 数据分片(Hash Slot)
- 高可用性
- 水平扩展
特点:
- 16384 个 Hash Slot
- 每个节点负责一部分 Slot
- 支持在线添加/删除节点
- 自动故障转移
Node.js 操作 Redis
客户端选择
| 客户端 | 特点 | 适用场景 |
|---|---|---|
| redis | 官方客户端,功能基础 | 简单场景 |
| ioredis | 功能丰富,支持集群 | 生产环境 |
| node-redis | 新版官方客户端,支持 Promise | 新项目 |
ioredis 示例
javascript
const Redis = require('ioredis');
// 单机连接
const redis = new Redis({
host: 'localhost',
port: 6379,
password: 'your-password',
db: 0
});
// 集群连接
const redisCluster = new Redis.Cluster([
{ host: '127.0.0.1', port: 7000 },
{ host: '127.0.0.1', port: 7001 },
{ host: '127.0.0.1', port: 7002 }
]);
// 基本操作
async function redisExample() {
// String
await redis.set('name', 'John');
const name = await redis.get('name');
console.log(name);
// Hash
await redis.hset('user:1', 'name', 'John', 'age', 30);
const user = await redis.hgetall('user:1');
console.log(user);
// List
await redis.lpush('messages', 'hello', 'world');
const messages = await redis.lrange('messages', 0, -1);
console.log(messages);
// Set
await redis.sadd('tags', 'nodejs', 'redis');
const tags = await redis.smembers('tags');
console.log(tags);
// Sorted Set
await redis.zadd('leaderboard', 100, 'user:1', 200, 'user:2');
const leaderboard = await redis.zrevrange('leaderboard', 0, -1, 'WITHSCORES');
console.log(leaderboard);
}
redisExample().catch(console.error);Pipeline 批量操作
javascript
// 使用 Pipeline 减少网络往返
const pipeline = redis.pipeline();
pipeline.set('key1', 'value1');
pipeline.set('key2', 'value2');
pipeline.get('key1');
const results = await pipeline.exec();
console.log(results);Pub/Sub 发布订阅
javascript
// 订阅者
const subscriber = new Redis();
subscriber.subscribe('news', (err, count) => {
console.log(`订阅了 ${count} 个频道`);
});
subscriber.on('message', (channel, message) => {
console.log(`频道 ${channel} 收到消息: ${message}`);
});
// 发布者
const publisher = new Redis();
publisher.publish('news', 'Hello Redis Pub/Sub!');实战场景
1. 会话存储
javascript
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
app.use(session({
store: new RedisStore({ client: redis }),
secret: 'your-secret-key',
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 24 * 60 * 60 * 1000
}
}));2. 缓存热点数据
javascript
async function getUser(id) {
const cacheKey = `user:${id}`;
// 先查缓存
const cachedUser = await redis.get(cacheKey);
if (cachedUser) {
return JSON.parse(cachedUser);
}
// 缓存未命中,查数据库
const user = await db.users.findByPk(id);
// 更新缓存
if (user) {
await redis.set(cacheKey, JSON.stringify(user), 'EX', 3600);
}
return user;
}3. 限流
javascript
async function rateLimit(key, limit, windowMs) {
const current = Date.now();
const windowStart = current - windowMs;
// 移除窗口外的记录
await redis.zremrangebyscore(key, 0, windowStart);
// 获取当前窗口内的请求数
const count = await redis.zcard(key);
if (count >= limit) {
return false; // 超过限制
}
// 添加当前请求
await redis.zadd(key, current, current);
// 设置过期时间
await redis.expire(key, Math.ceil(windowMs / 1000));
return true; // 未超过限制
}
// 使用
app.use(async (req, res, next) => {
const ip = req.ip;
const key = `rate_limit:${ip}`;
const allowed = await rateLimit(key, 60, 60000); // 1 分钟 60 次请求
if (!allowed) {
return res.status(429).json({ message: 'Too Many Requests' });
}
next();
});4. 分布式锁
javascript
async function withLock(key, expireTime, callback) {
const value = Date.now().toString();
try {
// 获取锁
const acquired = await redis.set(key, value, 'NX', 'EX', expireTime);
if (!acquired) {
throw new Error('Could not acquire lock');
}
// 执行回调
return await callback();
} finally {
// 释放锁
const script = `
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
`;
await redis.eval(script, 1, key, value);
}
}
// 使用
await withLock('order:123', 10, async () => {
// 处理订单,保证并发安全
await processOrder(123);
});面试高频问题
1. Redis 为什么这么快?
回答思路:
- 内存存储:数据全部在内存,读写速度快
- 单线程模型:避免线程切换和锁竞争
- I/O 多路复用:高效处理并发连接
- 精简数据结构:底层实现优化,占用空间小
- 网络模型:非阻塞 I/O,响应迅速
2. Redis 有哪些数据结构?各有什么用途?
回答思路:
- String:缓存、计数器、分布式锁
- List:消息队列、最新列表、栈/队列
- Hash:对象存储、用户信息
- Set:去重、交集并集差集
- Sorted Set:排行榜、延迟队列、范围查询
- HyperLogLog:基数统计
- Bitmap:布尔值存储
- Geo:地理位置
- Stream:消息队列(支持消费组)
3. Redis 持久化有哪些方式?区别是什么?
回答思路:
- RDB:定期生成快照,恢复速度快,适合备份
- AOF:记录所有写操作,数据更安全,恢复速度慢
- 混合持久化:结合 RDB 和 AOF 的优点,Redis 4.0+ 支持
4. 如何解决 Redis 缓存穿透、缓存击穿、缓存雪崩?
回答思路:
- 缓存穿透:布隆过滤器、空值缓存
- 缓存击穿:互斥锁、逻辑过期、永不过期
- 缓存雪崩:随机过期时间、多级缓存、限流降级
5. 如何实现 Redis 分布式锁?
回答思路:
- 基本实现:SET key value NX EX expireTime
- 释放锁:使用 Lua 脚本保证原子性
- Redlock 算法:多实例场景,提高可靠性
- 注意事项:锁过期时间、死锁、误释放
6. Redis 集群有哪些模式?
回答思路:
- 主从复制:数据备份、负载均衡
- 哨兵模式:监控、自动故障转移
- Redis Cluster:数据分片、水平扩展
7. Redis 内存淘汰策略有哪些?
回答思路:
- volatile-lru:从过期键中删除最近最少使用的
- allkeys-lru:从所有键中删除最近最少使用的
- volatile-ttl:从过期键中删除剩余 TTL 最小的
- volatile-random:从过期键中随机删除
- allkeys-random:从所有键中随机删除
- noeviction:不删除键,返回错误
8. Redis 与 Memcached 有什么区别?
回答思路:
- 数据结构:Redis 支持多种数据结构,Memcached 只支持字符串
- 持久化:Redis 支持 RDB 和 AOF,Memcached 不支持
- 集群:Redis 有官方集群方案,Memcached 需要客户端实现
- 内存管理:Redis 使用自己的内存分配器,Memcached 使用 slab 分配
- 性能:单线程 vs 多线程,各有优势
延伸阅读
官方文档
书籍
- 《Redis 设计与实现》by 黄健宏
- 《Redis 实战》by Josiah L. Carlson
- 《Redis 深度历险》by 钱文品
在线资源
工具
- Redis Desktop Manager
- RedisInsight
- ioredis:Node.js Redis 客户端
- node-redis:新版官方客户端