Redis 基础
问题
Redis 是什么?有哪些数据结构和应用场景?持久化、淘汰策略、集群方案如何选择?
答案
Redis(Remote Dictionary Server)是一个基于内存的高性能键值数据库,支持多种数据结构,常用于缓存、分布式锁、消息队列、排行榜等场景。
一、五种基础数据结构
1. String(字符串)
最基础的类型,值可以是字符串、数字或二进制数据(最大 512MB)。
import Redis from 'ioredis';
const redis = new Redis('redis://localhost:6379');
// 基本操作
await redis.set('name', 'Alice');
await redis.get('name'); // 'Alice'
// 带过期时间(EX = 秒,PX = 毫秒)
await redis.set('session:abc', JSON.stringify(userData), 'EX', 3600);
// 计数器(原子操作)
await redis.set('page:views', '0');
await redis.incr('page:views'); // 1
await redis.incrby('page:views', 10); // 11
// 分布式锁(SET NX:只在 key 不存在时设置)
await redis.set('lock:order:123', 'token', 'PX', 10000, 'NX');
// 位操作(用于签到、在线状态等)
await redis.setbit('sign:user:1:202603', 23, 1); // 第 24 天签到
await redis.bitcount('sign:user:1:202603'); // 统计签到天数
底层编码:短字符串(≤44 字节)用 embstr(一次内存分配),长字符串用 raw,纯数字用 int。
常见应用:缓存、计数器、分布式锁、Session 存储、限流(滑动窗口计数)。
2. Hash(哈希表)
键值对集合,适合存储对象。相比把整个对象 JSON 序列化为 String,Hash 可以单独读写某个字段,减少网络传输。
// 存储用户信息
await redis.hset('user:1', {
name: 'Alice',
age: '25',
email: 'alice@example.com',
avatar: 'https://cdn.example.com/avatar/1.jpg',
});
// 读取单个字段
const name = await redis.hget('user:1', 'name'); // 'Alice'
// 读取所有字段
const user = await redis.hgetall('user:1'); // { name: 'Alice', age: '25', ... }
// 只更新某个字段(不需要读取整个对象再写回)
await redis.hset('user:1', 'age', '26');
// 字段不存在时才设置
await redis.hsetnx('user:1', 'score', '0');
// 对数值字段原子增减
await redis.hincrby('user:1', 'score', 10);
底层编码:元素少(≤128)且值短(≤64 字节)时用 listpack(Redis 7.0+,之前是 ziplist),否则用 hashtable。
常见应用:对象存储(用户信息、商品详情)、购物车(user:cart:1 → { productId: quantity })。
// 方案 1:String(整体读写)
await redis.set('user:1', JSON.stringify({ name: 'Alice', age: 25 }));
// 修改 age 需要:get → parse → 修改 → stringify → set
// 方案 2:Hash(字段级读写)
await redis.hset('user:1', { name: 'Alice', age: '25' });
// 修改 age 直接:hset user:1 age 26
Hash 更省带宽、支持部分更新,适合字段多且经常只读写部分字段的场景。但如果对象很小且总是整体读写,String + JSON 更简单。
3. List(列表)
双向链表,支持从两端插入/弹出,适合队列和栈。
// 消息队列(生产者-消费者)
await redis.lpush('queue:tasks', JSON.stringify({ type: 'email', to: 'alice@example.com' }));
await redis.lpush('queue:tasks', JSON.stringify({ type: 'sms', to: '13800138000' }));
// 消费者阻塞等待(BRPOP:队列为空时阻塞,超时 30 秒)
const [key, value] = await redis.brpop('queue:tasks', 30) ?? [];
// 最新列表(只保留最新 100 条)
await redis.lpush('recent:articles', articleId);
await redis.ltrim('recent:articles', 0, 99); // 截断,只保留前 100 条
const latest = await redis.lrange('recent:articles', 0, 9); // 取最新 10 条
// 获取列表长度
const len = await redis.llen('queue:tasks');
底层编码:元素少时用 listpack,多时用 quicklist(多个 ziplist 组成的双向链表)。
常见应用:消息队列(简易版)、最新动态列表、操作日志。
List 做消息队列缺少 ACK 机制、消费者组、消息回溯等能力。生产环境推荐用 Redis Stream(Redis 5.0+)或专业消息队列(RabbitMQ、Kafka)。
4. Set(集合)
无序不重复集合,支持交集、并集、差集运算。
// 用户标签
await redis.sadd('tags:user:1', 'frontend', 'react', 'typescript', 'node');
await redis.sadd('tags:user:2', 'frontend', 'vue', 'typescript', 'python');
// 共同标签(交集)
const common = await redis.sinter('tags:user:1', 'tags:user:2');
// ['frontend', 'typescript']
// 用户 1 有但用户 2 没有的标签(差集)
const diff = await redis.sdiff('tags:user:1', 'tags:user:2');
// ['react', 'node']
// 所有标签合集(并集)
const all = await redis.sunion('tags:user:1', 'tags:user:2');
// 判断是否存在
const isMember = await redis.sismember('tags:user:1', 'react'); // 1(存在)
// 随机抽取(抽奖)
const winner = await redis.srandmember('lottery:pool', 3); // 随机取 3 个
// 集合大小
const size = await redis.scard('tags:user:1'); // 4
底层编码:元素少且都是整数时用 intset,否则用 hashtable。
常见应用:标签系统、共同好友/关注、去重、抽奖。
5. Sorted Set(有序集合)
每个元素关联一个 score(分数),按 score 排序,支持范围查询和排名。
// 排行榜
await redis.zadd('leaderboard', 1500, 'player:alice');
await redis.zadd('leaderboard', 2300, 'player:bob');
await redis.zadd('leaderboard', 1800, 'player:charlie');
// 前 10 名(分数从高到低)
const top10 = await redis.zrevrange('leaderboard', 0, 9, 'WITHSCORES');
// ['player:bob', '2300', 'player:charlie', '1800', 'player:alice', '1500']
// 查询某玩家排名(从 0 开始)
const rank = await redis.zrevrank('leaderboard', 'player:alice'); // 2(第 3 名)
// 增加分数
await redis.zincrby('leaderboard', 500, 'player:alice'); // 1500 → 2000
// 按分数范围查询
const mid = await redis.zrangebyscore('leaderboard', 1000, 2000, 'WITHSCORES');
// 延迟队列(score = 执行时间戳)
await redis.zadd('delay:queue', Date.now() + 60000, JSON.stringify(task)); // 60s 后执行
// 消费者轮询:取出 score ≤ 当前时间的任务
const ready = await redis.zrangebyscore('delay:queue', 0, Date.now(), 'LIMIT', 0, 10);
底层编码:元素少时用 listpack,多时用 skiplist(跳表)+ hashtable。
常见应用:排行榜、延迟队列、带权重的优先队列、时间线(Timeline)。
二、高级数据结构
Redis 还有几种专用数据结构,面试中偶尔涉及:
| 类型 | 说明 | 典型应用 |
|---|---|---|
| Stream | 持久化消息队列,支持消费者组、ACK、消息回溯 | 事件流、消息队列(替代 List) |
| HyperLogLog | 概率计数,用 12KB 内存估算上亿基数(误差 < 1%) | UV 统计、独立 IP 计数 |
| Bitmap | 位数组,按 bit 操作 | 签到日历、布隆过滤器、在线状态 |
| GEO | 地理位置,基于 Sorted Set | 附近的人、距离计算 |
// HyperLogLog — UV 统计
await redis.pfadd('uv:2026-03-24', 'user:1', 'user:2', 'user:3');
await redis.pfadd('uv:2026-03-24', 'user:1', 'user:4'); // user:1 重复不计
const uv = await redis.pfcount('uv:2026-03-24'); // ≈ 4
// GEO — 附近的人
await redis.geoadd('locations', 116.397128, 39.916527, 'user:1');
await redis.geoadd('locations', 116.405285, 39.904989, 'user:2');
// 查询 user:1 附近 5km 内的人
const nearby = await redis.georadius('locations', 116.397128, 39.916527, 5, 'km', 'WITHDIST');
// Stream — 消息队列
await redis.xadd('stream:events', '*', 'type', 'click', 'userId', '1');
// 创建消费者组
await redis.xgroup('CREATE', 'stream:events', 'group1', '0');
// 消费(阻塞读取)
const messages = await redis.xreadgroup('GROUP', 'group1', 'consumer1', 'COUNT', 10, 'BLOCK', 5000, 'STREAMS', 'stream:events', '>');
三、持久化
Redis 数据存在内存中,需要持久化到磁盘防止重启丢失。
RDB(Redis Database)
定时快照:按配置的时间间隔生成数据的二进制快照文件(dump.rdb)。
# redis.conf
save 900 1 # 900 秒内至少 1 次写操作就触发快照
save 300 10 # 300 秒内至少 10 次写操作
save 60 10000 # 60 秒内至少 10000 次写操作
过程:fork() 子进程 → 子进程遍历内存写 RDB 文件 → 完成后替换旧文件。利用操作系统的 Copy-on-Write(写时复制)机制,fork 期间父进程继续处理请求。
| 优点 | 缺点 |
|---|---|
| 文件紧凑,恢复速度快 | 两次快照之间的数据可能丢失 |
| 适合备份和灾难恢复 | fork 大数据集时可能短暂阻塞 |
AOF(Append Only File)
追加日志:将每条写命令追加到日志文件(appendonly.aof)。
# redis.conf
appendonly yes
appendfsync everysec # 每秒 fsync 一次(推荐,最多丢 1 秒数据)
# appendfsync always # 每条命令都 fsync(最安全,性能差)
# appendfsync no # 交给 OS(性能好,可能丢数据多)
AOF 重写:AOF 文件会越来越大,Redis 会定期触发重写(BGREWRITEAOF),将内存中的数据重新生成一份最小命令集,替换旧文件。
| 优点 | 缺点 |
|---|---|
| 数据更安全(最多丢 1 秒) | 文件比 RDB 大 |
| 人类可读,易于调试 | 恢复速度比 RDB 慢 |
混合持久化(推荐)
Redis 4.0+ 支持混合持久化(aof-use-rdb-preamble yes):AOF 重写时,前半部分用 RDB 格式(快速恢复),后半部分用 AOF 增量(减少数据丢失)。兼具 RDB 的快速恢复和 AOF 的数据安全。
四、内存淘汰策略
当 Redis 内存达到 maxmemory 限制时,会根据配置的策略淘汰 key。
| 策略 | 说明 | 适用场景 |
|---|---|---|
noeviction | 不淘汰,写入报错 | 数据不能丢的场景 |
allkeys-lru | 从所有 key 中淘汰最近最少使用的 | 最常用,通用缓存 |
allkeys-lfu | 从所有 key 中淘汰使用频率最低的 | 热点数据明显的场景 |
allkeys-random | 随机淘汰 | 各 key 访问概率相近 |
volatile-lru | 只从设了过期时间的 key 中 LRU 淘汰 | 部分 key 需要永久保留 |
volatile-lfu | 只从设了过期时间的 key 中 LFU 淘汰 | 同上 |
volatile-ttl | 淘汰剩余 TTL 最短的 key | 希望快过期的先走 |
volatile-random | 从设了过期时间的 key 中随机淘汰 | - |
- LRU(Least Recently Used):淘汰最久没被访问的。适合访问有时间局部性的场景
- LFU(Least Frequently Used):淘汰被访问次数最少的。适合有明显冷热差异的场景,不会被偶发的批量扫描污染
Redis 的 LRU 是近似 LRU(随机采样 N 个 key 取最旧的),不是精确 LRU,这样避免了维护全局链表的开销。
五、过期删除策略
Redis 不会在 key 过期的瞬间立即删除,而是组合使用两种策略:
| 策略 | 说明 | 触发时机 |
|---|---|---|
| 惰性删除 | 访问 key 时才检查是否过期,过期则删除 | 每次读写 key |
| 定期删除 | 每 100ms 随机抽查一批 key,删除其中已过期的 | 后台定时任务 |
两者配合:惰性删除保证访问到的 key 一定是有效的;定期删除清理不再被访问的过期 key,防止内存泄漏。如果仍有漏网之鱼(既没被访问也没被抽中),由内存淘汰策略兜底。
六、发布订阅(Pub/Sub)
Redis 内置的消息广播机制,发布者和订阅者解耦。
// 订阅者
const sub = new Redis();
sub.subscribe('channel:notifications');
sub.on('message', (channel, message) => {
console.log(`收到 ${channel}: ${message}`);
});
// 发布者
const pub = new Redis();
await pub.publish('channel:notifications', JSON.stringify({
type: 'new_message',
from: 'user:1',
}));
- 不持久化:消息发出后如果没有订阅者在线,消息就丢了
- 不支持 ACK:无法确认消息被消费
- 不支持消息回溯:新订阅者收不到历史消息
需要可靠消息传递时用 Redis Stream 或专业消息队列。
七、事务与 Lua 脚本
事务(MULTI/EXEC)
// MULTI/EXEC:将多条命令打包为原子执行
const pipeline = redis.multi();
pipeline.decrby('balance:user:1', 100); // 扣钱
pipeline.incrby('balance:user:2', 100); // 加钱
const results = await pipeline.exec(); // 原子执行
Redis 的 MULTI/EXEC 保证原子性(要么全执行要么全不执行),但不支持回滚——如果某条命令执行失败(如对 String 做 INCR),其他命令仍会执行。这和关系型数据库的事务不同。
Lua 脚本
需要更复杂的原子操作时用 Lua 脚本,整个脚本在 Redis 中原子执行:
// 示例:限流器(固定窗口)
const rateLimitScript = `
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local current = redis.call('INCR', key)
if current == 1 then
redis.call('EXPIRE', key, window)
end
if current > limit then
return 0 -- 超过限制
end
return 1 -- 允许通过
`;
const allowed = await redis.eval(rateLimitScript, 1, 'rate:user:1', '100', '60');
// key=rate:user:1, 60 秒内最多 100 次
八、分布式锁
class RedisLock {
constructor(private redis: Redis) {}
/**
* 获取锁
* SET NX PX:原子操作,key 不存在时设置并带过期时间
* token:随机值,用于释放时验证身份(只有持锁者才能释放)
*/
async acquire(key: string, ttl: number = 10000): Promise<string | null> {
const token = crypto.randomUUID();
const result = await this.redis.set(key, token, 'PX', ttl, 'NX');
return result === 'OK' ? token : null;
}
/**
* 释放锁
* 必须用 Lua 脚本保证原子性:检查 token 和删除 key 必须是原子的
* 否则可能出现:A 检查 token 匹配 → 锁过期 → B 获取锁 → A 删除了 B 的锁
*/
async release(key: string, token: string): Promise<boolean> {
const script = `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
`;
const result = await this.redis.eval(script, 1, key, token);
return result === 1;
}
}
// 使用
const lock = new RedisLock(redis);
const token = await lock.acquire('lock:order:123');
if (token) {
try {
await processOrder('123');
} finally {
await lock.release('lock:order:123', token);
}
}
单实例分布式锁在 Redis 主从切换时可能失效(主节点获取锁后宕机,从节点提升为主但没有锁数据)。Redis 作者提出了 Redlock 算法:向 N 个独立 Redis 实例同时请求锁,超过半数成功才算获取成功。但 Redlock 存在争议(Martin Kleppmann 的批评),生产中更常见的做法是用 ZooKeeper 或 etcd 做强一致性锁。
九、集群方案
| 方案 | 说明 | 适用场景 |
|---|---|---|
| 主从复制 | 主写从读,异步复制 | 读多写少、高可用 |
| Sentinel 哨兵 | 监控主从,自动故障转移 | 中小规模高可用 |
| Cluster 集群 | 数据分片(16384 个槽),去中心化 | 大数据量、高吞吐 |
主从复制
写请求 → [Master] → 异步复制 → [Slave 1](读)
→ 异步复制 → [Slave 2](读)
- 读写分离:Master 负责写,Slave 负责读
- 异步复制:Slave 的数据可能略滞后(最终一致性)
Sentinel 哨兵
[Sentinel 1] [Sentinel 2] [Sentinel 3]
↓ ↓ ↓
监控 Master + Slaves
Master 宕机 → 投票选举 → 提升 Slave 为新 Master
Redis Cluster
[Node 1: 槽 0-5460] [Node 2: 槽 5461-10922] [Node 3: 槽 10923-16383]
↕ ↕ ↕
[Slave 1] [Slave 2] [Slave 3]
- 数据按
CRC16(key) % 16384分配到不同节点 - 客户端通过
MOVED重定向找到正确节点 - 每个主节点有对应从节点做故障转移
十、Node.js 中使用 Redis 的最佳实践
import Redis from 'ioredis';
// 创建连接(生产环境推荐连接池)
const redis = new Redis({
host: 'localhost',
port: 6379,
password: process.env.REDIS_PASSWORD,
db: 0,
retryStrategy(times) {
// 重连策略:指数退避,最大 30 秒
return Math.min(times * 200, 30000);
},
maxRetriesPerRequest: 3,
enableReadyCheck: true,
lazyConnect: true, // 延迟连接,await redis.connect() 时才连
});
// 错误处理
redis.on('error', (err) => {
console.error('Redis error:', err);
});
// Pipeline 批量操作(减少网络往返)
const pipeline = redis.pipeline();
pipeline.set('key1', 'value1');
pipeline.set('key2', 'value2');
pipeline.get('key1');
const results = await pipeline.exec();
// [[null, 'OK'], [null, 'OK'], [null, 'value1']]
// 优雅关闭
process.on('SIGTERM', async () => {
await redis.quit(); // 等待所有命令执行完后关闭
});
常见面试问题
Q1: Redis 单线程为什么还这么快?
答案:
Redis 的"单线程"指的是命令执行在单线程中完成,但这并不意味着整个 Redis 只有一个线程。
快的原因:
- 纯内存操作:数据在内存中,读写速度是纳秒级,远快于磁盘
- 单线程避免锁竞争:不需要加锁、不需要上下文切换,消除了多线程的同步开销
- IO 多路复用(epoll/kqueue):一个线程监听多个 socket,不需要为每个连接创建线程
- 高效数据结构:跳表、压缩列表、intset 等都是精心优化的
- 简单的执行模型:没有查询解析、执行计划等开销(相比 SQL 数据库)
Redis 6.0 引入了多线程 IO——网络数据的读取和写入可以由多个 IO 线程并行处理,但命令执行仍然是单线程。这样既利用了多核处理网络 IO 瓶颈,又保持了命令执行的简单性和无锁特性。
IO 线程 1 ─┐
IO 线程 2 ──┼─→ [命令队列] → 主线程串行执行 → [响应队列] ──┼→ IO 线程写回
IO 线程 3 ─┘ └→
Q2: 缓存穿透、击穿、雪崩是什么?怎么解决?
答案:
| 问题 | 本质 | 场景 | 解决方案 |
|---|---|---|---|
| 穿透 | 查询不存在的数据,每次都打到 DB | 恶意攻击、大量无效 ID | 1. 布隆过滤器拦截不存在的 key 2. 缓存空值( set key "" EX 60) |
| 击穿 | 热点 key 过期瞬间大量请求打到 DB | 秒杀商品的缓存过期 | 1. 互斥锁(SET NX,只有一个请求去查 DB)2. 逻辑过期(value 中存过期时间,异步更新) |
| 雪崩 | 大量 key 同时过期,DB 瞬间压力暴增 | 缓存批量设置相同 TTL | 1. 过期时间加随机偏移(TTL + random(0, 300))2. 多级缓存(本地缓存 + Redis) 3. 限流降级 |
// 缓存穿透 — 缓存空值
async function getUser(id: string): Promise<User | null> {
const cached = await redis.get(`user:${id}`);
if (cached === '') return null; // 空值缓存命中,直接返回
if (cached) return JSON.parse(cached);
const user = await db.findUser(id);
if (!user) {
// 缓存空值,短 TTL 防止长期占用
await redis.set(`user:${id}`, '', 'EX', 60);
return null;
}
await redis.set(`user:${id}`, JSON.stringify(user), 'EX', 3600);
return user;
}
// 缓存击穿 — 互斥锁
async function getHotProduct(id: string): Promise<Product> {
const cached = await redis.get(`product:${id}`);
if (cached) return JSON.parse(cached);
// 获取互斥锁
const lockKey = `lock:product:${id}`;
const locked = await redis.set(lockKey, '1', 'PX', 5000, 'NX');
if (locked) {
try {
const product = await db.findProduct(id);
await redis.set(`product:${id}`, JSON.stringify(product), 'EX', 3600);
return product;
} finally {
await redis.del(lockKey);
}
} else {
// 没拿到锁,短暂等待后重试
await new Promise(r => setTimeout(r, 100));
return getHotProduct(id);
}
}
Q3: Redis 持久化怎么选?RDB 和 AOF 的区别?
答案:
| 维度 | RDB | AOF |
|---|---|---|
| 方式 | 定时快照(fork 子进程生成二进制文件) | 追加写命令日志 |
| 数据安全 | 可能丢失两次快照之间的数据 | 最多丢 1 秒(everysec) |
| 文件大小 | 紧凑 | 较大(但有重写机制) |
| 恢复速度 | 快(直接加载二进制) | 较慢(重放命令) |
| 适用场景 | 备份、灾难恢复 | 数据安全要求高 |
推荐:Redis 4.0+ 使用混合持久化(aof-use-rdb-preamble yes),AOF 重写时前半部分用 RDB 格式、后半部分用 AOF 增量,兼具 RDB 的快速恢复和 AOF 的数据安全。
Q4: Redis 和 Memcached 的区别?
答案:
| 维度 | Redis | Memcached |
|---|---|---|
| 数据结构 | 丰富(String/Hash/List/Set/ZSet/Stream...) | 仅 String |
| 持久化 | RDB + AOF | 不支持 |
| 集群 | 原生 Cluster + Sentinel | 客户端分片 |
| 线程模型 | 单线程执行(6.0+ 多线程 IO) | 多线程 |
| 事务 | MULTI/EXEC + Lua 脚本 | 不支持 |
| 发布订阅 | 支持 | 不支持 |
| 单 value 上限 | 512MB | 1MB |
选 Redis:需要丰富数据结构、持久化、集群(大多数场景)。选 Memcached:纯 KV 缓存、多线程利用多核、不需要持久化。
Q5: Redis 的分布式锁怎么实现?有什么问题?
答案:
基本实现:SET key token NX PX ttl(原子操作:key 不存在时设置,并带过期时间)。释放时用 Lua 脚本保证"检查 token + 删除 key"的原子性。
存在的问题:
| 问题 | 说明 | 解决方案 |
|---|---|---|
| 锁过期但业务没执行完 | 任务执行时间超过 TTL,锁自动释放 | 看门狗机制:后台线程定期续期 |
| 主从切换丢锁 | Master 获取锁后宕机,Slave 提升为 Master 但没有锁数据 | Redlock 算法(多实例投票) |
| 可重入性 | 同一线程/进程需要多次获取同一把锁 | 用 Hash 存储(key → { holder, count }) |
Q6: 如何保证缓存和数据库的一致性?
答案:
这是经典问题,核心是先更新 DB 还是先更新缓存。
| 策略 | 做法 | 问题 |
|---|---|---|
| Cache Aside(旁路缓存) | 读:先读缓存,miss 则读 DB 写入缓存 写:先更新 DB,再删除缓存 | 短暂不一致窗口,但最简单可靠 |
| Write Through(写穿) | 写操作由缓存层代理,同步写入 DB | 实现复杂 |
| Write Behind(写回) | 只写缓存,异步批量写 DB | 可能丢数据 |
推荐 Cache Aside 模式(先更新 DB,再删缓存):
async function updateUser(id: string, data: Partial<User>) {
// 1. 先更新数据库
await db.updateUser(id, data);
// 2. 再删除缓存(下次读取时回源 DB 并写入新缓存)
await redis.del(`user:${id}`);
}
删除缓存比更新缓存更安全:
- 并发场景下,两个写请求可能导致缓存和 DB 数据不一致(A 先写 DB 但后更新缓存,B 后写 DB 但先更新缓存)
- 删除是幂等的,多次删除没有副作用
- 避免了缓存中存在脏数据的风险
如果对一致性要求更高,可以加延迟双删(先删缓存 → 更新 DB → 延迟 N 毫秒再删一次)或用 Canal 监听 binlog 异步删除缓存。
详见 缓存与数据库一致性。
Q7: Redis 的 Pipeline 和事务(MULTI/EXEC)有什么区别?
答案:
| 维度 | Pipeline | MULTI/EXEC |
|---|---|---|
| 作用 | 批量发送命令,减少网络往返(RTT) | 保证多条命令原子执行 |
| 原子性 | ❌ 无原子性,命令可能被其他客户端的命令穿插 | ✅ 命令打包后原子执行 |
| 回滚 | - | ❌ 不支持回滚 |
| 性能 | 显著减少 RTT,提升吞吐量 | 有一定开销 |
| 使用场景 | 批量读写、初始化数据 | 需要保证多个操作的原子性 |
Pipeline 是网络优化,MULTI/EXEC 是执行语义。两者可以组合使用。
Q8: Redis 集群方案怎么选?
答案:
| 方案 | 架构 | 数据分片 | 自动故障转移 | 适用场景 |
|---|---|---|---|---|
| 主从复制 | 1 主 N 从 | ❌ | ❌(手动切换) | 读多写少,手动运维 |
| Sentinel | 主从 + 哨兵集群 | ❌ | ✅ | 中小规模高可用 |
| Cluster | 多主多从,16384 槽 | ✅ | ✅ | 大数据量、高吞吐 |
选择建议:
- 数据量小(< 几 GB)、需要高可用 → Sentinel
- 数据量大、需要水平扩展 → Cluster
- 只需读写分离、手动运维可接受 → 主从复制
Q9: Redis 的 key 过期了会立即删除吗?
答案:
不会。Redis 组合使用两种删除策略:
- 惰性删除:访问 key 时才检查是否过期,过期则删除并返回空。保证读到的一定是有效的
- 定期删除:每 100ms 随机抽查 20 个设了过期时间的 key,删除其中已过期的。如果过期比例 > 25%,立即再抽查一轮
两者配合既避免了遍历所有 key 的 CPU 开销,又防止了大量过期 key 长期占用内存。漏网之鱼由内存淘汰策略(maxmemory-policy)兜底。
Q10: Redis 6.0 为什么引入多线程?
答案:
Redis 的性能瓶颈不在 CPU(命令执行很快),而在网络 IO——高并发下,单线程读写 socket 成为瓶颈。
Redis 6.0 的多线程只用于网络 IO(读取请求、写回响应),命令执行仍然是单线程。这样:
- 利用多核处理网络 IO,吞吐量提升 1-2 倍
- 命令执行仍是单线程,无需加锁,保持了 Redis 的简单性
- 默认关闭,需要手动开启(
io-threads 4)