Skip to content

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 有哪些数据结构?

结构底层实现常用命令典型场景
StringSDS(简单动态字符串)SET/GET/INCR缓存、计数器、分布式锁
Hash压缩列表 / 哈希表HSET/HGET/HGETALL对象存储(用户信息)
List快速列表(quicklist)LPUSH/RPOP/LRANGE消息队列、最新列表
Set整数集合 / 哈希表SADD/SINTER/SUNION标签、共同好友
Sorted Set压缩列表 / 跳表ZADD/ZRANGE/ZRANK排行榜、延迟队列
StreamRax 树(基数树)XADD/XREAD/XGROUP消费者组消息队列
BitmapString 的位操作SETBIT/BITCOUNT签到、在线状态
HyperLogLog概率算法PFADD/PFCOUNTUV 统计
GEOSorted 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 为什么早期选择单线程?

  1. Redis 瓶颈在内存和网络,不在 CPU:命令执行时间基本在微秒级,单线程足够快
  2. 避免并发问题:单线程天然线程安全,无需锁、无需考虑数据竞争
  3. 代码简洁:实现简单,易于维护和调试
  4. 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:

  1. 排除不健康的 Slave:下线、断连超过 5 秒、ping 无响应
  2. 选择优先级高的slave-priority 配置值越小优先级越高
  3. 选择复制偏移量最大的:数据最完整
  4. 选择 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 故障并进行故障转移

故障转移:

  1. 某个 Master 被认为客观下线(半数以上 Master 同意)
  2. 该 Master 的 Slave 发起选举,成为新 Master
  3. 新 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.ADDBF.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-lruallkeys-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 memorymem_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/DISCARDBEGIN → 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 1
java
// 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 底层数据结构?

对外数据结构底层实现
StringSDS(简单动态字符串)
Hash压缩列表(ziplist)→ 哈希表(dict)
List快速列表(quicklist)
Set整数集合(intset)→ 哈希表(dict)
Sorted Set压缩列表(ziplist)→ 跳表(skiplist)+ 哈希表(dict)
StreamRax 树(基数树)

小数据量时使用压缩列表 / 整数集合节省内存,超过阈值自动转为哈希表 / 跳表。

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 keyspaceINFO 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 替代。