Appearance
Redis 面试题
基础篇
1. 说说什么是 Redis?
Redis(Remote Dictionary Server)是一个基于内存的 高性能 Key-Value 数据库,C 语言编写,支持多种数据结构,常用于缓存、消息队列、分布式锁等场景。
核心特点:内存存储(快)、单线程模型(避免锁竞争)、IO 多路复用(高并发)、持久化(数据不丢失)、丰富的数据结构。
2. Redis 可以用来干什么?
| 场景 | 说明 | 实现手段 |
|---|---|---|
| 缓存 | 热点数据缓存,降低 DB 压力 | String + 过期时间 |
| 分布式锁 | 跨进程/跨机器的互斥锁 | SETNX + Lua |
| 消息队列 | 异步解耦、削峰填谷 | List / Stream |
| 排行榜 | 实时排名 | Sorted Set |
| 计数器 | 点赞、浏览量、限流计数 | String incr |
| 会话管理 | 分布式 Session 共享 | String + 过期 |
| 社交关系 | 好友关系、共同关注 | Set 交集/并集 |
| 地理位置 | 附近的人、距离计算 | GEO |
3. Redis 有哪些数据结构?
| 结构 | 底层实现 | 常用命令 | 典型场景 |
|---|---|---|---|
| String | SDS(简单动态字符串) | SET/GET/INCR | 缓存、计数器、分布式锁 |
| Hash | 压缩列表 / 哈希表 | HSET/HGET/HGETALL | 对象存储(用户信息) |
| List | 快速列表(quicklist) | LPUSH/RPOP/LRANGE | 消息队列、最新列表 |
| Set | 整数集合 / 哈希表 | SADD/SINTER/SUNION | 标签、共同好友 |
| Sorted Set | 压缩列表 / 跳表 | ZADD/ZRANGE/ZRANK | 排行榜、延迟队列 |
| Stream | Rax 树(基数树) | XADD/XREAD/XGROUP | 消费者组消息队列 |
| Bitmap | String 的位操作 | SETBIT/BITCOUNT | 签到、在线状态 |
| HyperLogLog | 概率算法 | PFADD/PFCOUNT | UV 统计 |
| GEO | Sorted Set 封装 | GEOADD/GEORADIUS | 地理位置计算 |
4. Redis 为什么快呢?
一句话:纯内存操作 + 单线程无锁 + IO 多路复用 + 高效数据结构。
| 原因 | 说明 |
|---|---|
| 纯内存操作 | 数据在内存中,读写速度远超磁盘,纳秒级响应 |
| 单线程模型 | 避免多线程的上下文切换和锁竞争开销 |
| IO 多路复用 | 单线程监听多个连接,非阻塞 IO 处理大量并发 |
| 高效数据结构 | 底层用 SDS、跳表、压缩列表等精心优化的结构 |
5. 能说一下 I/O 多路复用吗?
IO 多路复用是一种让单个线程同时监听多个网络连接的机制。Redis 服务端只需要一个线程,就能同时处理成千上万个客户端连接。
传统阻塞 IO:
线程1 ─── 等待连接1 数据到达 ─── 处理连接1
线程2 ─── 等待连接2 数据到达 ─── 处理连接2
→ 一个连接需要一个线程,连接多了线程爆炸
IO 多路复用:
单个线程 ─── select/poll/epoll 同时监听所有连接
│
├─ 连接1 有数据 → 处理连接1
├─ 连接2 有数据 → 处理连接2
└─ 连接3 无数据 → 跳过
→ 一个线程处理所有连接,谁有数据处理谁Redis 使用 epoll(Linux)/ kqueue(macOS)/ select(Windows)三种机制,根据操作系统自动选择最优。
6. Redis 为什么早期选择单线程?
- Redis 瓶颈在内存和网络,不在 CPU:命令执行时间基本在微秒级,单线程足够快
- 避免并发问题:单线程天然线程安全,无需锁、无需考虑数据竞争
- 代码简洁:实现简单,易于维护和调试
- CPU 不是瓶颈:单个 Redis 实例 QPS 可达 10W+,瓶颈通常在网络带宽,不在 CPU
7. Redis 6.0 使用多线程是怎么回事?
Redis 6.0 的多线程只用于网络 IO 读写,命令执行仍是单线程。
Redis 6.0 之前(全单线程):
读请求 → 解析命令 → 执行命令 → 写响应 → 返回客户端
└──────────────── 全部单线程 ────────────────┘
Redis 6.0 之后(IO 多线程):
读请求 → 解析命令 → 执行命令 → 写响应 → 返回客户端
└─ 多线程 ─┘ └ 单线程 ┘ └─ 多线程 ─┘核心原则:命令执行保持单线程,所以仍然没有并发安全问题。 IO 多线程只是为了解决网络带宽瓶颈,默认关闭,需通过 io-threads 参数开启。
持久化篇
8. Redis 持久化方式有哪些?有什么区别?
| 方式 | 机制 | 文件 | 恢复速度 | 数据安全性 |
|---|---|---|---|---|
| RDB | 快照:定期将内存数据拍快照写入磁盘 | dump.rdb(二进制) | 快 | 可能丢失最后一次快照后的数据 |
| AOF | 日志:记录每次写命令到日志文件 | appendonly.aof(文本) | 慢 | 更安全,最多丢失 1 秒数据 |
| 混合持久化(4.0+) | RDB 快照 + AOF 增量日志 | aof 文件(RDB 头 + AOF 尾) | 快 | 安全 |
9. RDB 和 AOF 各自有什么优缺点?
RDB 优点:
- 文件紧凑,适合备份和灾难恢复
- 恢复大数据集时速度比 AOF 快
- 对 Redis 性能影响小(fork 子进程处理)
RDB 缺点:
- 两次快照之间的数据会丢失
- fork 子进程时如果数据量大,可能阻塞主进程
AOF 优点:
- 数据安全性高,最多丢失 1 秒数据
- 文件可读,可手动修复误操作
- 自动重写(rewrite)压缩文件
AOF 缺点:
- 相同数据集下 AOF 文件比 RDB 大
- 恢复速度比 RDB 慢
- 每秒刷盘(fsync)对性能有少量影响
10. RDB 和 AOF 如何选择?
| 场景 | 推荐 |
|---|---|
| 数据可以丢失几分钟 | 只用 RDB |
| 需要每秒钟的数据安全性 | RDB + AOF |
| 对数据一致性要求极高 | 只用 AOF(everysec) |
| 追求极致恢复速度 | RDB + 混合持久化(4.0+) |
| 生产环境通用推荐 | RDB + AOF 同时开启,4.0+ 开启混合持久化 |
11. Redis 的数据恢复?
Redis 启动
│
▼
┌ 是否开启 AOF? ──┐
│ 是 │ 否
▼ ▼
加载 AOF 文件 加载 RDB 文件
(数据更完整) (恢复更快)- 如果 AOF 和 RDB 都开启,优先加载 AOF 文件(数据更完整)
- 加载过程中 Redis 处于阻塞状态,直到数据全部加载完成
- AOF 文件损坏时可通过
redis-check-aof --fix修复
12. Redis 4.0 的混合持久化了解吗?
混合持久化 = RDB 快照(头部) + AOF 增量日志(尾部)
AOF 文件结构:
┌─────────────────────────────────────────────────────┐
│ RDB 格式的二进制数据快照(某个时间点) │
│ → 快速恢复大部分数据 │
├─────────────────────────────────────────────────────┤
│ AOF 格式的增量命令日志(快照之后的新写入) │
│ → 补充快照之后的数据变更 │
└─────────────────────────────────────────────────────┘优点:兼具 RDB 的快速恢复和 AOF 的数据安全性。恢复时先加载 RDB 部分获得大部分数据,再重放 AOF 部分获得增量,速度远快于纯 AOF。
主从复制篇
13. 主从复制了解吗?
主从复制是将一台 Redis 服务器(Master)的数据复制到其他 Redis 服务器(Slave),实现读写分离和数据冗余。
- Master:负责写操作,并将数据同步给 Slave
- Slave:负责读操作,从 Master 同步数据,默认只读
14. Redis 主从有几种常见的拓扑结构?
| 结构 | 图示 | 特点 |
|---|---|---|
| 一主一从 | Master → Slave | 最简单,读写分离,无高可用 |
| 一主多从 | Master → Slave1, Slave2, ... | 读扩展,分担读压力 |
| 树状结构 | Master → Slave → Slave | 减轻 Master 复制压力,但延迟增加 |
| 哨兵模式 | Sentinel + 一主多从 | 自动故障转移,高可用 |
| 集群模式 | 多主多从 | 水平扩展,数据分片 |
15. Redis 的主从复制原理了解吗?
1. 连接建立
Slave → Master: 发送 PSYNC 命令(包含 replid + offset)
首次连接 replid 为 ?,offset 为 -1
2. 数据同步
┌─ 全量同步(首次连接或 replid 不匹配)
│ Master 执行 BGSAVE 生成 RDB → 发送给 Slave
│ → Slave 清空数据 → 加载 RDB → Master 再发送缓冲区中的增量命令
│
└─ 增量同步(断线重连后 replid 匹配)
Master 发送 Replication Buffer 中 offset 之后的命令
3. 命令传播
Master 执行写命令 → 异步发送给所有 Slave → Slave 执行相同命令16. 说说主从数据同步的方式?
| 同步方式 | 触发条件 | 过程 |
|---|---|---|
| 全量同步 | 首次连接 / replid 不匹配 / offset 不在缓冲区 | Master 生成 RDB → 发送 → Slave 加载 |
| 增量同步 | 断线重连,replid 匹配且 offset 在缓冲区 | Master 发送 offset 之后的增量命令 |
| 命令传播 | 正常运行时 | Master 每执行一条写命令,同步给 Slave |
17. 主从复制存在哪些问题呢?
| 问题 | 说明 | 解决 |
|---|---|---|
| 数据不一致 | 异步复制,Master 挂了数据可能未同步到 Slave | 尽量 min-replicas-to-write 保证至少 N 个 Slave 同步 |
| 复制延迟 | 网络延迟导致 Slave 数据滞后 | 监控 offset 差值 |
| 主库宕机 | 无法自动切换,需要手动提主 | 哨兵 / 集群 |
| 写压力大 | 所有写操作都在 Master | 集群分片 |
哨兵篇
18. Redis Sentinel(哨兵)了解吗?
Sentinel 是 Redis 的高可用解决方案,核心功能:监控、通知、自动故障转移。
┌──────────────────────────────────────────────────────────┐
│ Sentinel 集群 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Sentinel1│ │ Sentinel2│ │ Sentinel3│ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │ │ │ │
│ └──────────────┼──────────────┘ │
│ │ 监控 │
│ ▼ │
│ ┌──────────────┐ │
│ │ Master │ ← 挂掉 → 自动选举新 Master │
│ └──────┬───────┘ │
│ ┌──────────┼──────────┐ │
│ ▼ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Slave 1 │ │ Slave 2 │ │ Slave 3 │ │
│ └──────────┘ └──────────┘ └──────────┘ │
└──────────────────────────────────────────────────────────┘19. Redis Sentinel 实现原理知道吗?
Sentinel 通过三个定时任务完成监控和故障转移:
| 任务 | 频率 | 作用 |
|---|---|---|
| 心跳检测 | 每 1 秒 | 对所有 Master/Slave/Sentinel 发 PING |
| 信息交换 | 每 2 秒 | 通过 Master 的 __sentinel__:hello 频道交换信息 |
| 状态检查 | 每 10 秒 | 检查 Master 的 INFO 信息,更新 Slave 列表 |
主观下线(SDOWN):单个 Sentinel 认为 Master 不可达(PING 超时)。客观下线(ODOWN):超过 quorum 个 Sentinel 都认为 Master 不可达,触发故障转移。
20. 领导者 Sentinel 节点选举了解吗?
Raft 共识算法:每个 Sentinel 都可以发起投票请求,第一个获得半数以上票数的 Sentinel 成为 Leader。
1. 确认 Master 客观下线后,Sentinel 们开始选举 Leader
2. 每个 Sentinel 向其他 Sentinel 发送选举请求(Epoch 纪元号)
3. 先到先得:每个 Sentinel 每轮只能投一票
4. 获得超过半数 Sentinel 投票的成为 Leader
5. 如果没人过半,增加 Epoch 重新选举21. 新的主节点是怎样被挑选出来的?
按优先级从高到低过滤,找到第一个符合条件的 Slave:
- 排除不健康的 Slave:下线、断连超过 5 秒、ping 无响应
- 选择优先级高的:
slave-priority配置值越小优先级越高 - 选择复制偏移量最大的:数据最完整
- 选择 runid 最小的:字典序最小,保证确定性
集群篇
22. Redis 集群了解吗?
Redis Cluster 是去中心化的分布式方案,解决单机内存和并发瓶颈。
┌──────────────────────────────────────────────────────────┐
│ Redis Cluster │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Master 0 │ │ Master 1 │ │ Master 2 │ │
│ │ Slot 0-5460 │ │ Slot 5461- │ │ Slot 10923- │ │
│ │ │ │ 10922 │ │ 16383 │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Slave 0 │ │ Slave 1 │ │ Slave 2 │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└──────────────────────────────────────────────────────────┘特点:16384 个哈希槽(Slot) 分布在不同 Master 上,客户端可请求任意节点,节点自动重定向到正确节点。
23. 集群中数据如何分区?
方案一:节点取余分区
hash(key) % N → 节点编号- 优点:简单
- 缺点:扩容/缩容时几乎所有数据都要迁移(N 变了,映射全变)
方案二:一致性哈希分区
环形哈希空间(0 ~ 2^32-1),节点均匀分布在环上。
key 落在环上,顺时针找到第一个节点。- 优点:扩容/缩容只影响相邻节点
- 缺点:节点分布不均可能造成数据倾斜,需要虚拟节点
方案三:虚拟槽分区(Redis 采用)
hash(key) = CRC16(key) & 16383 → 0~16383 的槽位号
每个 Master 负责一部分槽位,槽位可以在节点间迁移- 优点:数据迁移以槽为单位,粒度适中,不影响其他槽
- 这是 Redis Cluster 官方方案
24. 能说说 Redis 集群的原理吗?
集群创建(Gossip 协议):
- 节点间通过 Gossip 协议交换状态(PING/PONG)
- 每个节点维护其他节点的状态(在线/疑似下线/已下线)
- 所有节点共同决定 Master 故障并进行故障转移
故障转移:
- 某个 Master 被认为客观下线(半数以上 Master 同意)
- 该 Master 的 Slave 发起选举,成为新 Master
- 新 Master 接管槽位,广播给其他节点
25. 说说集群的伸缩?
扩容(添加新节点):
1. 启动新节点,加入集群
2. 使用 redis-cli --cluster reshard 将部分槽位迁移到新节点
3. 槽位迁移过程:源节点 → 目标节点,逐槽迁移,不阻塞服务缩容(下线节点):
1. 使用 redis-cli --cluster reshard 将下线节点的槽位迁移到其他节点
2. 使用 redis-cli --cluster del-node 从集群中移除节点缓存问题篇
26. 什么是缓存击穿、缓存穿透、缓存雪崩?
| 问题 | 描述 | 原因 | 影响 |
|---|---|---|---|
| 缓存击穿 | 单个热点 key 过期,大量请求打到 DB | 热点 key 过期瞬间 | 单个 DB 压力暴增 |
| 缓存穿透 | 查询不存在的数据,缓存和 DB 都没有 | 恶意攻击 / 业务 bug | 所有请求穿透到 DB |
| 缓存雪崩 | 大量 key 同时过期,请求全部打到 DB | 设置了相同的过期时间 | DB 瞬间压力过大,可能宕机 |
解决方案:
| 问题 | 解决方案 |
|---|---|
| 缓存击穿 | 热点 key 永不过期 / 互斥锁(SETNX 重建)、逻辑过期 |
| 缓存穿透 | 布隆过滤器 / 缓存空值(设置短过期时间)/ 参数校验 |
| 缓存雪崩 | 过期时间加随机值 / 多级缓存 / 熔断降级 / 限流 |
27. 能说说布隆过滤器吗?
布隆过滤器(Bloom Filter)是一种概率型数据结构,用于判断"某个元素一定不存在,或可能存在"。
原理:
1. 维护一个位数组(bit array),初始全为 0
2. 一个元素通过 K 个哈希函数映射到 K 个位置,将对应位置设为 1
3. 查询时计算 K 个位置,如果全部为 1 → 可能存在,如果有一个为 0 → 一定不存在
特性:
一定不存在:100% 准确
可能存在:有误判率(取决于哈希函数数量和位数组大小)
不能删除(Counting Bloom Filter 可以)Redis 中使用: Redis 4.0 提供了 BF.ADD、BF.EXISTS 等原生命令(通过 RedisBloom 模块),也可以基于 Bitmap 手动实现。
28. 如何保证缓存和数据库数据的一致性?
选择合适的缓存更新策略:
| 策略 | 操作 | 风险 |
|---|---|---|
| 先更新 DB,再删除缓存(推荐) | 写 DB → 删除缓存 | 查询可能在删除缓存前读到旧值(概率极低) |
| 先删除缓存,再更新 DB | 删缓存 → 写 DB → 再次删缓存(延迟双删) | 复杂,两次删除之间的查询可能写入旧缓存 |
| 先更新 DB,再更新缓存 | 写 DB → 写缓存 | 并发写入时顺序可能错乱,导致数据不一致 |
缓存不一致处理:
- 设置过期时间:兜底方案,最终一致性
- 订阅 Binlog(Canal):监听 DB 变更,异步更新缓存
- 消息队列:DB 更新后发 MQ 消息,异步更新缓存
29. 如何保证本地缓存和分布式缓存的一致?
| 方案 | 说明 |
|---|---|
| 广播通知(MQ / Redis Pub/Sub) | 数据变更时广播所有服务实例,清空本地缓存 |
| 极短过期时间 | 本地缓存 TTL 设短(如 1-3 秒),容忍短暂不一致 |
| 版本号机制 | 缓存写入时带版本号,读取时校验版本号 |
| Caffeine + Redis 两级缓存 | Caffeine 本地缓存 → 未命中查 Redis → 未命中查 DB |
30. 怎么处理热 key?
| 方案 | 说明 |
|---|---|
| 本地缓存 | 热点 key 缓存到服务本地(Caffeine),减少 Redis 压力 |
| 多副本 | 将热点 key 复制到多个 Redis 分片,读取时随机选择副本 |
| 读写分离 | 热点 key 从 Slave 读,分散压力 |
| 提前预热 | 活动开始前将热点数据加载到缓存 |
| 限流降级 | 对热点 key 做限流,超过阈值返回兜底数据 |
31. 缓存预热怎么做呢?
缓存预热是指在系统启动或活动开始前,提前将热点数据加载到缓存中。
| 方式 | 实现 |
|---|---|
| 启动时加载 | 项目启动后,通过定时任务或 PostConstruct 从 DB 加载热点数据到缓存 |
| 定时任务 | 定时从 DB 同步数据到缓存,保持缓存"温"状态 |
| 流量回放 | 用 Nginx 日志回放历史请求,触发缓存预热 |
| 预热脚本 | 独立脚本在发布前批量写入缓存 |
32. 热点 key 重建?问题?解决?
问题: 热点 key 过期瞬间,大量请求同时触发缓存重建,导致缓存击穿。
解决方案:
java
// 互斥锁方案:只有一个线程去重建缓存
public String get(String key) {
String value = redis.get(key);
if (value == null) {
// SETNX 抢锁,唯一线程去重建
if (redis.setnx(key + ":lock", "1", 10, TimeUnit.SECONDS)) {
try {
value = db.query(key);
redis.set(key, value, 30, TimeUnit.MINUTES);
} finally {
redis.del(key + ":lock");
}
} else {
// 没抢到锁,等一会重试
Thread.sleep(50);
return get(key);
}
}
return value;
}33. 无底洞问题?如何解决?
无底洞问题: 集群节点越多,一次批量操作(如 MGET)需要访问的节点越多,网络 IO 开销越大,性能反而下降。
| 方案 | 说明 |
|---|---|
| 优化 key 设计 | 让相关 key 落在同一槽位(使用 Hash Tag {key}) |
| 串行 IO | 按节点分组 key,一次请求一个节点 |
| 并行 IO | 多线程并行请求不同节点,汇总结果 |
| 减少批量操作 | 非必要不用 MGET,改为逐个 GET(反而可能更快) |
内存管理篇
34. Redis 报内存不足怎么处理?
| 处理方式 | 说明 |
|---|---|
| 增加内存 | 加大 maxmemory 配置或扩容机器 |
| 设置过期时间 | 确保所有 key 都有合理的 TTL |
| 调整淘汰策略 | 设置合适的 maxmemory-policy |
| 大 key 治理 | 拆分大 key、压缩 value |
| 数据分片 | 集群模式,将数据分散到多个节点 |
| 冷热分离 | 冷数据迁移到磁盘存储(SSDB / Pika) |
35. Redis 的过期数据回收策略有哪些?
| 策略 | 说明 |
|---|---|
| 惰性删除 | 访问 key 时检查是否过期,过期则删除(CPU 友好,内存不友好) |
| 定期删除 | 每 100ms 随机抽取一批 key 检查并删除过期 key(折中方案) |
Redis 默认同时使用惰性删除 + 定期删除,两者配合保证性能和内存平衡。
36. Redis 有哪些内存溢出控制/内存淘汰策略?
当 Redis 内存达到 maxmemory 时,根据 maxmemory-policy 淘汰数据:
| 策略 | 范围 | 说明 |
|---|---|---|
noeviction | 全部 | 不淘汰,写请求报错(默认) |
allkeys-lru | 全部 key | 淘汰最近最少使用的(推荐) |
allkeys-lfu | 全部 key | 淘汰最不经常使用的(4.0+) |
allkeys-random | 全部 key | 随机淘汰 |
volatile-lru | 有过期时间的 key | 淘汰最近最少使用的 |
volatile-lfu | 有过期时间的 key | 淘汰最不经常使用的(4.0+) |
volatile-random | 有过期时间的 key | 随机淘汰 |
volatile-ttl | 有过期时间的 key | 淘汰 TTL 最短的 |
生产环境推荐:
allkeys-lru或allkeys-lfu。
37. Redis 阻塞?怎么解决?
| 阻塞原因 | 说明 | 解决 |
|---|---|---|
| 大 key 删除 | DEL 大 key 阻塞主线程(大 key 删除是 O(n)) | UNLINK 异步删除(4.0+) |
| AOF 刷盘阻塞 | fsync 时磁盘 I/O 慢 | 换 SSD,调大 fsync 间隔 |
| RDB fork 阻塞 | fork 子进程时复制页表 | 控制单个实例内存 < 10GB |
| 命令复杂度高 | KEYS、SMEMBERS、HGETALL 等 O(n) 命令 | 用 SCAN 替代 KEYS,控制集合大小 |
| CPU 竞争 | 绑核 / 其他进程争抢 CPU | 绑定 CPU 核心(taskset) |
| 慢查询 | 复杂度高的命令 | 开启 slowlog 监控,优化命令 |
38. 大 key 问题了解吗?
定义: 单个 key 的 value 过大(String > 10KB,集合元素 > 10000 个)。
危害:
- 网络阻塞:传输大 value 占用大量带宽
- 内存不均:集群中某些节点内存过高
- 删除阻塞:DEL 大 key 会阻塞主线程
- 迁移困难:集群迁移槽位时大 key 导致迁移慢
解决:
- 拆分大 key(如按用户 ID 分片)
- 压缩 value(如用 Protobuf 替代 JSON)
- UNLINK 异步删除(4.0+)
- 避免使用 KEYS、HGETALL 等 O(n) 命令
39. Redis 常见性能问题和解决方案?
| 问题 | 排查 | 解决 |
|---|---|---|
| 慢查询 | SLOWLOG GET 10 | 优化命令,避免 KEYS/SMEMBERS 等 |
| 网络延迟 | ping 延迟 | 同机房部署,使用长连接 |
| CPU 飙升 | INFO CPU | 绑核、减少 fork 频率 |
| 内存碎片 | INFO memory 的 mem_fragmentation_ratio | 重启实例 / 开启 activedefrag(4.0+) |
| 连接数过多 | INFO clients | 连接池、限制最大连接数 |
| 带宽瓶颈 | 网卡流量监控 | 压缩数据、拆分大 key、集群 |
高级特性篇
40. 使用 Redis 如何实现异步队列?
List 方式(简单队列):
java
// 生产者
redis.lpush("task_queue", task);
// 消费者
while (true) {
String task = redis.brpop(0, "task_queue"); // 阻塞等待
process(task);
}Stream 方式(消费者组,5.0+):
java
// 生产者
redis.xadd("task_stream", "*", "task", taskJson);
// 消费者组
redis.xgroup("task_stream", "worker_group", ">", ...);Stream 支持消费者组、消息确认、消息回溯,是更完善的消息队列方案。
41. Redis 如何实现延时队列?
Sorted Set 方案:
java
// 添加延迟任务:score = 执行时间戳
redis.zadd("delay_queue", executeTime, taskJson);
// 定时扫描:获取到期的任务
Set<String> tasks = redis.zrangeByScore("delay_queue", 0, System.currentTimeMillis());
for (String task : tasks) {
// 先移除再处理(原子性)
if (redis.zrem("delay_queue", task) > 0) {
process(task);
}
}42. Redis 支持事务吗?
支持,但与关系型数据库的事务不同:
| 特性 | Redis 事务 | MySQL 事务 |
|---|---|---|
| 原子性 | 不保证(命令入队错误则全部不执行,运行时错误则部分执行) | 保证 |
| 隔离性 | 单线程天然隔离 | 多级别隔离 |
| 回滚 | 不支持(DISCARD 只是取消,不是回滚) | 支持 |
| 命令 | MULTI → 命令入队 → EXEC/DISCARD | BEGIN → SQL → COMMIT/ROLLBACK |
java
redis.multi();
redis.set("key1", "value1");
redis.incr("counter");
redis.exec(); // 一起执行,中间不会被其他命令打断43. Redis 和 Lua 脚本的使用了解吗?
作用: Lua 脚本在 Redis 中原子执行,不会被其他命令打断,适用于需要多条命令原子性完成的场景。
lua
-- 限流脚本:同一个 IP 每秒最多 10 次请求
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local current = redis.call('INCR', key)
if current == 1 then
redis.call('EXPIRE', key, 1)
end
if current > limit then
return 0 -- 限流
end
return 1 -- 放行优点:原子性、减少网络开销、可复用。 缺点:调试困难、阻塞主线程(脚本执行期间其他命令等待)。
44. Redis 的管道了解吗?
管道(Pipeline)允许客户端一次性发送多条命令,不需要等待每条命令的响应,减少网络往返次数(RTT)。
普通模式:
发送命令1 → 等待响应1 → 发送命令2 → 等待响应2 → 发送命令3 → 等待响应3
RTT x 3
管道模式:
发送命令1 → 发送命令2 → 发送命令3 → 收到响应1 → 收到响应2 → 收到响应3
RTT x 1java
// Jedis Pipeline
Pipeline pipeline = jedis.pipelined();
pipeline.set("key1", "value1");
pipeline.set("key2", "value2");
pipeline.set("key3", "value3");
pipeline.sync(); // 一次性发送所有命令注意:管道不是原子的,命令之间可能被其他客户端命令插入。
45. Redis 实现分布式锁了解吗?
基础版(SETNX + 过期时间):
java
// 加锁
String lockKey = "lock:order:" + orderId;
boolean locked = redis.setnx(lockKey, "1", 30, TimeUnit.SECONDS);
if (locked) {
try {
// 业务逻辑
} finally {
redis.del(lockKey); // 释放锁
}
}问题: 如果线程 A 还没执行完锁就过期了,线程 B 拿到锁,A 执行完会删除 B 的锁。
改进版(Lua 脚本保证原子性):
java
// 加锁时设置唯一标识(UUID)
String lockValue = UUID.randomUUID().toString();
boolean locked = redis.setnx(lockKey, lockValue, 30, TimeUnit.SECONDS);
// 释放锁时验证标识(Lua 脚本)
String lua = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
redis.eval(lua, Collections.singletonList(lockKey), Collections.singletonList(lockValue));Redisson 方案(推荐):
- 自动续期(Watch Dog):锁快过期时自动延长
- 可重入锁
- 红锁(RedLock)保证高可用
java
RLock lock = redisson.getLock("lock:order:" + orderId);
lock.lock(30, TimeUnit.SECONDS);
try {
// 业务逻辑
} finally {
lock.unlock();
}底层原理篇
46. 说说 Redis 底层数据结构?
| 对外数据结构 | 底层实现 |
|---|---|
| String | SDS(简单动态字符串) |
| Hash | 压缩列表(ziplist)→ 哈希表(dict) |
| List | 快速列表(quicklist) |
| Set | 整数集合(intset)→ 哈希表(dict) |
| Sorted Set | 压缩列表(ziplist)→ 跳表(skiplist)+ 哈希表(dict) |
| Stream | Rax 树(基数树) |
小数据量时使用压缩列表 / 整数集合节省内存,超过阈值自动转为哈希表 / 跳表。
47. Redis 的 SDS 和 C 中字符串相比有什么优势?
| 对比 | C 字符串 | SDS |
|---|---|---|
| 获取长度 | O(n) 遍历 | O(1) 直接读 len 字段 |
| 缓冲区溢出 | 可能(strcat 不检查) | 不会(自动扩容) |
| 内存分配 | 每次修改都要重新分配 | 预分配 + 惰性释放 |
| 二进制安全 | 遇到 \0 截断 | 二进制安全(以 len 判断结束) |
| 兼容 C API | 兼容 | 兼容(buf 末尾有 \0) |
48. 字典是如何实现的?Rehash 了解吗?
Redis 字典使用哈希表实现,通过渐进式 rehash 解决扩容问题。
字典结构:
dict {
ht[0] ← 当前使用的哈希表
ht[1] ← rehash 时的目标哈希表(平时为空)
rehashidx ← -1 表示不在 rehash,>=0 表示 rehash 进度
}
渐进式 rehash:
1. 为 ht[1] 分配空间(扩容为 2 倍,缩容为 1/2)
2. 设置 rehashidx = 0
3. 每次增删改查操作时,将 ht[0][rehashidx] 的键值对迁移到 ht[1]
4. 迁移完成后,ht[1] 变为 ht[0],rehashidx = -1渐进式 rehash 将一次性的迁移工作分摊到每次操作中,避免阻塞。
49. 跳跃表是如何实现的?原理?
跳跃表是一种多层有序链表,通过随机层高实现 O(log n) 的查找效率。
Level 3: 1 ───────────────────────────────→ 100
Level 2: 1 ────────────→ 30 ──────────────→ 100
Level 1: 1 ───→ 10 ───→ 30 ───→ 50 ───→ 80 ───→ 100
Level 0: 1 → 5 → 10 → 20 → 30 → 40 → 50 → 70 → 80 → 100
查找 50:
从最高层开始:
Level 3: 1 → 100,50 在中间,下降
Level 2: 1 → 30 → 100,50 在 30 和 100 之间,从 30 下降
Level 1: 30 → 50,找到!特点:
- 插入时随机生成层高(概率算法,类似抛硬币)
- 不需要像 B+Tree 那样做再平衡
- 实现简单,适合内存操作
- 范围查询时退化为链表遍历
50. 压缩列表了解吗?
压缩列表(ziplist)是一块连续内存,用紧凑的方式存储多个元素,节省内存但操作复杂。
压缩列表结构:
┌────────┬────────┬─────────┬───────┬───────┬───┬───────┬────────┐
│ zlbytes│ zltail │ zllen │entry1 │entry2 │...│entryN │ zlend │
│ 总字节 │尾偏移 │ 元素数 │ │ │ │ │ 结束标记│
└────────┴────────┴─────────┴───────┴───────┴───┴───────┴────────┘- 优点:极致节省内存(没有指针开销)
- 缺点:连锁更新(插入/删除可能导致后续元素内存重分配)
51. 快速列表 quicklist 了解吗?
quicklist = 双向链表 + 压缩列表,是 Redis 3.2 后 List 的底层实现。
quicklist 结构:
┌──────────┐ ┌──────────┐ ┌──────────┐
│ quicklist│→→→ │ quicklist│→→→ │ quicklist│
│ Node │ │ Node │ │ Node │
│ ┌──────┐ │ │ ┌──────┐ │ │ ┌──────┐ │
│ │ziplist│ │ │ │ziplist│ │ │ │ziplist│ │
│ └──────┘ │ │ └──────┘ │ │ └──────┘ │
└──────────┘ └──────────┘ └──────────┘- 链表解决插入/删除效率问题(O(1) 头尾操作)
- 压缩列表解决内存碎片问题(紧凑存储)
- 每个节点默认 8KB,可配置
list-max-ziplist-size
52. 假如 Redis 里面有 1 亿个 key,如何高效统计 key 的数量和类型?
不能直接用 KEYS * 或 DBSIZE 遍历所有 key! 1 亿个 key 使用 KEYS 会阻塞主线程数分钟。
| 方案 | 说明 |
|---|---|
| SCAN 游标遍历 | SCAN 0 COUNT 1000 分批迭代,不阻塞,统计类型和数量 |
| INFO keyspace | INFO keyspace 查看每个 DB 的 key 总数(瞬时值) |
| 离线分析 | RDB 文件解析工具(redis-rdb-tools)离线分析 |
| BigKeys 扫描 | redis-cli --bigkeys 扫描大 key 和类型分布 |
| 业务拆分 | 按业务模块使用不同 DB 或前缀,缩小统计范围 |
bash
# 使用 SCAN 安全统计 1 亿 key 的类型分布
redis-cli --scan --pattern '*' | awk -F: '{print $1}' | sort | uniq -c核心原则:生产环境永远不要用
KEYS *,用SCAN替代。
