一、基础与应用场景
1. Redis 通常应用于哪些场景?
参考答案:
Redis 是一个以内存为主、支持持久化的高性能键值数据库,常见场景包括:
- 缓存:热点数据、页面片段、查询结果等。
- 分布式会话:保存登录态、Token 和 Session。
- 计数与限流:访问次数、点赞数、接口频率限制。
- 排行榜:利用 ZSet 按分数排序。
- 消息通信:使用 Pub/Sub、List 或 Stream。
- 分布式锁:使用
SET key value NX PX,必要时配合 Lua。 - 延迟任务:使用 ZSet 保存任务执行时间。
- 地理位置:使用 GEO 记录坐标、计算距离及附近成员。
- 实时统计:使用 Bitmap、HyperLogLog 等结构。
Redis 适合低延迟、高并发场景,但不能因为速度快就无条件替代关系型数据库;是否使用要结合数据可靠性、查询模型和容量评估。
2. Redis 为什么这么快?
参考答案:
主要原因有:
- 数据主要存放在内存中,避免了大多数磁盘随机 I/O。
- 核心命令执行路径主要采用单线程模型,减少线程切换和锁竞争。
- 使用 SDS、哈希表、跳表、ListPack 等高效数据结构。
- 使用 I/O 多路复用,一条线程可以管理大量网络连接。
- Redis 协议简单,命令处理链路短。
- Redis 6.0 起可使用 I/O 线程处理网络读写,但命令执行仍以单线程为主。
要注意:单个慢命令、Big Key 或大量返回数据仍会阻塞主线程。
3. 为什么 Redis 设计为单线程?6.0 版本为何引入多线程?
参考答案:
Redis 早期的核心命令执行采用单线程,原因是内存操作本身很快,性能瓶颈更多来自网络 I/O;单线程还能避免加锁、线程切换和并发控制的复杂度,并天然保证单条命令的原子执行。
Redis 6.0 引入的是网络 I/O 多线程:可让多个线程并行读取请求和写回响应,减轻高并发下的网络处理压力。命令解析后的主要执行过程仍由主线程串行完成,因此并不是所有命令都变成了多线程执行。此外,后台持久化、异步释放等任务本来就会使用子进程或后台线程。
4. Redis 的 I/O 多路复用原理是什么?
参考答案:
Redis 使用事件循环配合 I/O 多路复用,让一个线程监听大量客户端连接。操作系统通过 epoll、kqueue 等机制通知 Redis 哪些连接已经可读或可写,Redis 只处理就绪的连接,不需要为每个连接创建一个线程,也不用在无数据时阻塞等待。
它解决的是"一个线程如何高效管理大量网络连接"的问题,并不表示多个命令会被同时执行。Redis 6.0 起可用 I/O 线程并行完成部分网络读写,命令执行仍主要由主线程串行完成。
5. Redis 中常见的数据类型有哪些?
参考答案:
五种基础类型:
- String:字符串、整数、浮点数或二进制数据。
- Hash:字段---值映射,适合对象属性。
- List:有序字符串列表,可从两端操作。
- Set:无序、元素唯一的集合。
- ZSet(Sorted Set):元素唯一,并按
score排序。
常用扩展能力还包括 Bitmap、Bitfield、HyperLogLog、GEO、Stream;Redis Stack 还提供 JSON、搜索等模块能力。
6. Redis String 类型的底层实现是什么?(SDS)
参考答案:
Redis 的字符串主要使用 SDS(Simple Dynamic String,简单动态字符串)实现,而不是直接使用 C 语言以 \0 结尾的字符数组。SDS 的结构中会记录已使用长度、可用空间和字符缓冲区。
它的优势包括:
- 获取长度是 O(1),不需要遍历。
- 二进制安全,可以保存图片、序列化数据及
\0字节。 - 扩容时会进行空间预分配,减少频繁内存分配。
- 缩短字符串时通常保留空闲空间,便于后续复用。
- API 会检查容量,降低缓冲区溢出风险。
不同长度的 SDS 会使用不同头部结构,以减少元数据占用。
7. Redis 字符串类型的最大值大小是多少?
参考答案:
单个 String 值的理论最大长度是 512 MB。实际项目中不应把值设计得接近该上限,因为超大值会带来网络耗时、主线程阻塞、复制延迟、持久化压力和内存峰值问题。通常应拆分数据,并重点治理 Big Key。
8. Redis 的 Hash 是什么?
参考答案:
Hash 是一个键下面的字段---值映射,类似 Java 的 Map<String, String>,适合存储用户信息、商品属性等对象数据。例如:
redis
HSET user:1001 name Alice age 20
HGET user:1001 name
小型 Hash 在新版本中通常使用 ListPack 紧凑存储;字段或值超过配置阈值后会转换为哈希表,以获得平均 O(1) 的字段访问性能。Hash 能对单个字段读写,通常比把整个对象序列化为一个 String 更节省更新成本,但不适合复杂条件查询。
9. Redis 的渐进式 Rehash 是如何实现的?
参考答案:
Redis 字典通常维护两个哈希表。开始扩容或收缩时,为第二个表分配新空间,并记录 rehashidx。之后不会一次性迁移全部数据,而是在每次增删改查以及定时任务中迁移少量桶,逐步把旧表元素移动到新表。
Rehash 期间,查询会同时检查两个表;新增元素只写入新表;删除和更新需要检查相应位置。迁移完成后释放旧表并重置状态。这样可以把 O(N) 的集中迁移成本分摊到多次操作,降低单次停顿时间。
10. Redis 的 Hash 冲突如何解决?
参考答案:
Redis 字典使用链地址法解决哈希冲突:多个哈希值落到同一个桶时,通过链表连接冲突节点。插入通常把新节点放在链表头部,因此平均插入接近 O(1)。
当负载因子升高时,Redis 会触发扩容并通过渐进式 Rehash 把节点迁移到更大的表中,降低冲突概率。若正在执行 RDB 或 AOF 重写,Redis 可能适当提高扩容门槛,以减少写时复制产生的额外内存。
11. Redis List 类型的常见操作命令有哪些?
参考答案:
- 插入:
LPUSH、RPUSH、LINSERT。 - 弹出:
LPOP、RPOP,以及阻塞版本BLPOP、BRPOP。 - 查询:
LRANGE、LINDEX、LLEN。 - 修改:
LSET、LTRIM。 - 原子移动:
LMOVE、BLMOVE;旧版本常见RPOPLPUSH。
两端插入、删除通常是 O(1),按索引访问或范围遍历可能是 O(N)。生产环境要避免对超长 List 执行大范围 LRANGE。
12. 如何在 Redis 中实现队列和栈数据结构?
参考答案:
- 队列(FIFO):生产者
LPUSH,消费者RPOP;需要阻塞等待时用BRPOP。 - 栈(LIFO):生产者
LPUSH,消费者LPOP。
List 方案简单,但可靠消息能力有限。若要求消费确认、消费者组、消息重放和 Pending List,应优先考虑 Redis Stream;若业务要求严格消息可靠性、复杂路由和大规模堆积,则通常使用专业消息队列。
13. Redis 中跳表的实现原理是什么?
参考答案:
跳表是在有序链表上增加多级索引。最底层包含全部节点,上层按随机规则抽取部分节点;查找时从最高层向右移动,越过目标后下降一层,直到最底层定位目标。
Redis 的 ZSet 跳表节点保存成员、分数、后退指针,以及每层的前进指针和跨度。跨度用于计算排名。查找、插入和删除的平均时间复杂度都是 O(log N),范围遍历也很方便。Redis 使用随机层高,避免复杂的严格平衡操作。
14. Redis 的 ZSet 实现原理是什么?
参考答案:
ZSet 同时满足"成员唯一"和"按分数排序"。
- 元素较少且元素较小时,新版本通常使用 ListPack 紧凑编码。
- 超过阈值后使用"哈希表 + 跳表":哈希表用于根据成员 O(1) 查分数,跳表用于按
score排序、范围查询和排名操作。
若分数相同,则按成员的字典序排列。ZADD、ZREM、ZRANK 等典型操作通常为 O(log N),范围返回还要加上返回元素数量的成本。
15. 为什么 Redis ZSet 使用跳表而不是红黑树、B+ 树?
参考答案:
跳表、红黑树都能提供 O(log N) 的平均查找和更新性能,但跳表的实现更直观,插入和删除无需旋转;定位到起点后,范围遍历可沿底层链表顺序进行,并且通过跨度容易支持排名。
B+ 树更适合磁盘或页式存储,通过高分支因子减少磁盘 I/O;Redis 的数据主要在内存中,没有同样的页访问优势。这里不是说跳表绝对优于其他结构,而是它与 Redis 的内存有序集合、范围查询和排名需求更匹配。
16. Redis 的 Geo 数据结构是什么?
参考答案:
GEO 用于保存经纬度并支持距离和附近位置查询,例如 GEOADD、GEODIST、GEOSEARCH。其底层建立在 ZSet 上:经纬度经过 GeoHash 编码后作为分数存储,成员作为 ZSet 元素。
GeoHash 会把二维坐标映射为一维编码,相邻区域的编码通常具有相近前缀。查询附近成员时先筛选候选网格,再进行距离计算。它适合"附近门店"等场景,但不是完整的 GIS 系统,复杂空间关系仍应使用专业地理数据库。
17. Redis Stream 的原理、消费者组和 Pending List 是什么?
参考答案:
Stream 是 Redis 提供的持久化消息结构。每条消息拥有递增 ID,可通过 XADD 写入、XRANGE 查询,支持消息历史、阻塞读取和按长度裁剪。与 Pub/Sub 不同,消费者离线后仍可读取已经保存的消息。
消费者组通过 XGROUP 创建,组内多个消费者分担消息。XREADGROUP 读取后,尚未确认的消息进入 Pending Entries List(PEL);处理成功后使用 XACK 确认。可通过 XPENDING 检查积压,并用 XCLAIM 或 XAUTOCLAIM 转移超时消息。Stream 能提供至少一次处理语义,消费者仍需做好幂等和故障重试。
18. Bitmap、HyperLogLog 和 Set 分别适合什么统计场景?
参考答案:
- Bitmap:适合用户 ID 范围相对紧凑的签到、在线状态和精确去重;支持位运算,但最大偏移决定空间占用。
- HyperLogLog:适合海量 UV 等基数估算,内存固定且很小,标准误差约 0.81%,但不能返回成员明细。
- Set:提供精确去重,并能保存和查询具体成员,还支持交并差集;数据量大时内存成本较高。
选择时主要看是否必须精确、是否需要成员明细、ID 是否连续以及可接受的内存成本。
二、紧凑结构、内存与性能
19. Redis 中 ZipList 和 QuickList 数据结构的特点是什么?
参考答案:
ZipList(压缩列表)是一段连续内存,节点紧凑排列,元数据开销低,适合存储少量、小元素;缺点是插入、删除可能引起内存移动,旧设计还可能发生连锁更新。
QuickList 是 List 的经典底层实现,可理解为"双向链表 + 紧凑列表":每个链表节点内部保存一段 ZipList,兼顾两端操作效率和内存局部性,还支持对中间节点压缩。
版本差异:Redis 7 已用 ListPack 替代 ZipList,并以 QuickList 节点承载 ListPack。面试时最好同时说明历史实现和当前实现。
20. Redis 的 ListPack 数据结构是什么?
参考答案:
ListPack 是 Redis 用来替代 ZipList 的紧凑顺序结构。它在一块连续内存中存储多个字符串或整数,每个元素携带自身编码及长度信息,末尾还记录当前元素总长度,便于反向遍历。
与 ZipList 相比,ListPack 不依赖"前一个节点长度"字段,因此避免了某个节点变长时触发后续节点连续扩容的连锁更新问题。它内存利用率高,适合小集合;元素很多时,中间插入、删除仍可能产生内存移动成本。
21. Redis 中 EMBSTR 对象的阈值设置为何为 44?其调整历史是什么?
参考答案:
embstr 会把 Redis 对象头和 SDS 内容一次性分配在同一块内存中,以减少分配次数并提高缓存局部性。经典 64 位环境下,Redis 3.2 之后的阈值常见为 44 字节:对象头、SDS 头、字符串、终止符等加起来可落在 jemalloc 的 64 字节分配档位内。
更早版本常见阈值是 39 字节;SDS 头部结构优化后提高到 44。超过阈值通常使用 raw 编码,整数值还可能使用 int 编码。这个数字与具体版本和内存布局有关,面试中应说明它不是任意业务常量。
22. Redis 中的内存碎片化是什么?如何进行优化?
参考答案:
内存碎片是 Redis 使用的内存与操作系统实际分配给进程的内存之间存在空洞或不连续空间。频繁新增、删除、扩缩容以及分配器的不同 size class 都可能产生碎片。通常可观察 INFO memory 中的 used_memory、used_memory_rss 和 mem_fragmentation_ratio,但比率过高或过低都要结合实例规模判断。
优化方法包括:
- 开启并合理配置主动碎片整理
activedefrag(前提是构建和版本支持)。 - 避免大量大小差异明显、生命周期极短的对象反复创建和删除。
- 控制 Big Key,采用更均匀的数据拆分方式。
- 在低峰期进行从节点重建、故障切换或滚动重启,以重新整理内存。
- 同时排查客户端缓冲区、复制缓冲区、Lua 缓存等非数据内存,不能只看碎片率。
23. Redis 中有哪些内存淘汰策略?
参考答案:
当内存达到 maxmemory 后,由 maxmemory-policy 决定行为:
noeviction:不淘汰,可能增长内存的写命令返回错误。allkeys-lru:在所有键中近似淘汰最久未使用的键。volatile-lru:只在设置了过期时间的键中按近似 LRU 淘汰。allkeys-lfu:在所有键中近似淘汰访问频率最低的键。volatile-lfu:只在有过期时间的键中按近似 LFU 淘汰。allkeys-random:在所有键中随机淘汰。volatile-random:在有过期时间的键中随机淘汰。volatile-ttl:优先淘汰剩余生存时间较短的键。
如果选择 volatile-* 但没有可过期键,其行为会接近 noeviction。纯缓存场景常见 allkeys-lru 或 allkeys-lfu,最终选择应依据访问分布。
24. Redis 数据过期后的删除策略是什么?
参考答案:
Redis 主要组合使用:
- 惰性删除:访问键时检查是否过期,过期则删除。
- 定期删除:后台周期性抽样检查设置了过期时间的键,并删除其中的过期键;若过期比例较高会继续检查,但会限制时间,避免长期阻塞。
Redis 不对每个键设置独立定时器,因为这会带来很高的时间和内存开销。过期删除与达到 maxmemory 后的内存淘汰是两套不同机制。
25. Redis 中的 Big Key 问题是什么?如何解决?
参考答案:
Big Key 指值很大,或集合中成员数量很多的键。它没有绝对统一阈值,通常需要按业务延迟和网络容量定义。Big Key 可能导致命令长时间阻塞、网络拥塞、删除卡顿、主从复制延迟、RDB/AOF 压力和故障切换变慢。
排查可使用 redis-cli --bigkeys、--memkeys、MEMORY USAGE、慢日志和采样扫描。治理方法:
- 将大对象按业务维度拆成多个键。
- 分页或分批读写,避免一次返回全部成员。
- 删除时使用
UNLINK异步释放;集合也可先SCAN分批删除。 - 对不需要精确明细的统计使用 Bitmap、HyperLogLog 等结构。
- 设置合理过期时间、监控大小增长,并在设计阶段限制单键规模。
26. 如何解决 Redis 中的热点 Key 问题?
参考答案:
热点 Key 是访问量集中到少数键,导致某个 Redis 节点、网卡或 CPU 成为瓶颈。可通过监控命令访问量、客户端采样、redis-cli --hotkeys(需要 LFU 策略)及代理层统计来识别。
常见方案:
- 在应用进程中增加短 TTL 的本地缓存,并配合失效通知或版本号。
- 将一个只读热点值复制成多个带后缀的副本,客户端随机读取。
- 对可拆分的数据按业务维度分片;计数可先分桶写入再汇总。
- 对请求做合并、限流和降级,防止缓存重建时流量击穿。
- 增加只读副本只能分担读流量,且要接受复制延迟和陈旧读。
热点 Key 和 Big Key 是不同问题,但同一个键可能同时具备两者。
27. Redis 性能瓶颈如何处理?
参考答案:
应先定位再扩容:
- 看
INFO、延迟监控、慢日志、CPU、内存、网络、连接数和复制延迟。 - 排查 O(N) 命令、大范围返回、Lua 长脚本、Big Key、热 Key、
KEYS等阻塞操作。 - 优化数据结构和命令,使用批量/管道减少往返,但控制单批大小。
- 调整连接池、超时、持久化和内存策略,避免客户端缓冲区膨胀。
- 读多场景可增加副本,容量或写负载过高可用 Cluster 分片。
- 最后再考虑升级 CPU、网络、内存或实例规格。
还应执行 LATENCY DOCTOR、观察系统换页和透明大页等主机因素。Redis 延迟问题不能只靠"加机器"解决。
28. Redis 常见命令的时间复杂度是多少?
参考答案:
面试中常见的复杂度如下:
- String 的
GET、SET通常是 O(1)。 - Hash、Set 的单字段或单成员操作平均是 O(1)。
- List 两端操作是 O(1),按索引访问和中间位置操作可能是 O(N)。
- ZSet 的添加、删除、排名通常是 O(log N),范围查询还要加返回元素数量 M,即 O(log N + M)。
KEYS、SMEMBERS、大范围LRANGE等可能是 O(N)。
复杂度只是第一步;即使标注 O(1),操作一个超大值或返回大量网络数据也可能造成明显阻塞。
29. KEYS 和 SCAN 有什么区别?
参考答案:
KEYS pattern 会一次遍历整个键空间并返回全部匹配键,时间复杂度 O(N),数据量大时可能长时间阻塞主线程,生产环境应谨慎使用。
SCAN 使用游标分批迭代,每次只返回部分结果,单次阻塞时间较短,适合在线扫描。但完整扫描期间若数据发生变化,结果可能重复,也可能不能反映某个严格一致时刻;客户端应处理重复,并一直迭代到游标回到 0。Hash、Set、ZSet 分别有 HSCAN、SSCAN、ZSCAN。
30. DEL 和 UNLINK 有什么区别?
参考答案:
DEL 从键空间删除键并在主线程同步释放相关内存。删除小键很快,但删除包含大量元素的集合可能造成明显停顿。
UNLINK 会先快速把键从键空间移除,再由后台 Lazy Free 线程异步回收大部分内存,更适合删除 Big Key。两者对客户端而言都会让键立即不可见,但实际内存下降可能有延迟。是否对过期或淘汰使用异步释放,还可以通过 lazyfree-* 配置控制。
31. Redis 如何查找慢命令和阻塞命令?
参考答案:
可以按以下顺序排查:
- 使用
SLOWLOG GET查看超过阈值的命令,并合理设置slowlog-log-slower-than和slowlog-max-len。 - 使用
LATENCY DOCTOR、LATENCY LATEST检查 fork、AOF、过期删除等延迟事件。 - 结合
INFO commandstats、客户端侧耗时和监控判断高频或高耗时命令。 - 排查 Big Key、热 Key、Lua 长脚本、
KEYS、大范围集合命令及超大响应。 - 检查
blocked_clients、持久化、网络、CPU、内存换页和宿主机资源竞争。
MONITOR 开销很高,只应在可控环境短时间使用,不能长期在线开启。
32. Redis CPU 突然达到 100% 如何排查?
参考答案:
先确认是 Redis 主线程、I/O 线程还是宿主机其他进程占用 CPU,再结合 INFO、慢日志和命令统计排查:
- 是否出现
KEYS、大范围查询、集合运算、排序或 Lua 长脚本。 - 是否存在热 Key 或请求量突然上升。
- 是否大量键同时过期,或触发主动碎片整理。
- 是否正在进行持久化、复制全量同步或频繁连接建立。
- 是否出现异常客户端重试和命令风暴。
处理方式包括停止或限流异常流量、优化慢命令、拆分热 Key、分批处理、修复客户端重试策略,必要时做分片扩容。不要未经定位就直接重启,否则容易丢失现场并再次复发。
33. Redis 内存持续上涨如何排查?
参考答案:
先使用 INFO memory 区分数据内存、RSS、客户端缓冲区、复制积压缓冲区、脚本缓存和内存碎片,再检查:
- 键数量或平均值大小是否持续增长,是否遗漏 TTL。
- 是否产生 Big Key、某类业务键泄漏或过期时间设置错误。
- 慢消费者是否导致输出缓冲区膨胀。
- 主从断连、AOF 重写和 COW 是否造成短期内存峰值。
- 内存已经释放但 RSS 未立即归还操作系统,是否属于分配器碎片。
可通过采样 SCAN、MEMORY USAGE、redis-cli --memkeys 和业务键前缀统计定位。治理包括补充 TTL、拆分或异步删除大键、限制客户端缓冲区、调整淘汰策略和容量扩展。
三、持久化、复制与高可用
34. Redis 的持久化机制有哪些?
参考答案:
主要有 RDB 和 AOF:
- RDB:在某个时间点生成数据快照。文件紧凑、恢复快,适合备份;但两次快照之间的数据可能丢失,生成快照时还可能出现写时复制带来的内存峰值。
- AOF:记录写命令,常见刷盘策略为
always、everysec、no。数据丢失窗口通常更小,但文件和写入开销一般更大,需要 AOF 重写压缩。 - 混合持久化:AOF 重写时以 RDB 格式保存基线,再追加 AOF 增量,兼顾加载速度和数据完整性。
生产环境经常同时开启 RDB 与 AOF,并将备份复制到独立故障域。复制本身不等于备份。
35. Redis 在生成 RDB 文件时如何处理请求?
参考答案:
执行 BGSAVE 时,主进程 fork 子进程,由子进程生成 RDB;主进程继续处理客户端请求。父子进程最初共享内存页,当父进程修改数据时由操作系统通过写时复制(Copy-on-Write)创建新页,因此子进程看到的是 fork 时刻的一致快照。
需要注意:fork 本身会短暂阻塞;写入很多时 COW 会显著增加内存;磁盘 I/O 也可能影响延迟。直接执行 SAVE 会阻塞主线程,生产环境通常不使用。Redis 7 的部分后台任务实现细节有所演进,但 RDB 快照的一致性核心仍是 fork/COW。
36. AOF 重写的原理是什么?
参考答案:
AOF 文件不断追加写命令后会越来越大。AOF 重写不是简单压缩旧文件,而是根据当前内存中的最终数据状态生成一份更精简的新 AOF,例如同一个键多次修改后只保留恢复最终状态所需的命令。
传统实现会 fork 后台子进程生成新文件,主进程继续处理请求,并记录重写期间的新写入;后台完成后再把增量合并并原子替换旧文件。开启混合持久化时,新文件前半部分通常是 RDB 格式的基线,后面追加 AOF 增量。重写要关注 fork 停顿、COW 内存峰值和磁盘 I/O。
37. AOF 的三种刷盘策略如何选择?
参考答案:
appendfsync always:每条写命令都刷盘,数据丢失窗口最小,但吞吐和延迟开销最大。appendfsync everysec:通常每秒刷盘一次,性能和可靠性较均衡,故障时理论上可能丢失约 1 秒数据,是最常用策略。appendfsync no:交给操作系统决定何时刷盘,性能较好,但丢失窗口更不可控。
选择取决于业务允许的数据丢失量和延迟目标。即使使用 always,也不能替代跨故障域备份和业务层可靠性设计。
38. RDB 和 AOF 同时开启时,Redis 启动优先加载哪个?
参考答案:
在传统持久化模式下,如果 AOF 开启且文件有效,Redis 启动时通常优先加载 AOF,因为 AOF 一般比 RDB 保存得更新;只有未开启 AOF 时才加载 RDB。
Redis 7 的 AOF 使用多文件目录和 Manifest 管理基础文件与增量文件,加载逻辑仍以有效的 AOF 持久化数据集为主。启动失败时应检查日志和文件完整性,不能看到 RDB 就直接删除损坏的 AOF;应先备份,再使用对应版本的检查修复工具评估。
39. fork 和写时复制为什么可能造成内存峰值?
参考答案:
Redis fork 子进程生成 RDB 或重写 AOF 后,父子进程最初共享相同物理内存页。父进程继续处理写请求时,被修改的页会复制出新页,这就是 Copy-on-Write。写入越多、修改越分散,需要复制的内存页就越多,因此实际内存可能远高于平时的 used_memory。
此外,fork 需要复制页表,数据集越大,fork 停顿越明显。生产环境要预留内存、降低重写期间的写入峰值、监控 latest_fork_usec 和 COW 指标,并避免宿主机发生 swap 或被 OOM Killer 终止。
40. Redis 有哪些可能导致数据丢失的场景?
参考答案:
- 只使用 RDB 时,故障会丢失最近一次快照后的写入。
- AOF 使用
everysec或no时,未刷盘数据可能丢失。 - 主从复制是异步的,主节点写入尚未复制就故障,副本提升后会丢失该写入。
- 网络分区或脑裂期间,旧主接受的写入在恢复后可能被覆盖。
- 达到内存上限后,淘汰策略主动删除缓存数据。
- 键正常过期、误执行删除或错误的批量运维命令。
- 磁盘损坏、AOF 尾部异常、错误恢复或备份策略失效。
降低风险需要合理配置 AOF/RDB、主从和最小副本写入限制,并建立异地备份、恢复演练、权限控制及业务幂等。Redis 高可用不等于数据零丢失。
41. Redis 主从复制的实现原理是什么?
参考答案:
副本首次连接主节点时会尝试使用复制 ID 和 offset 做部分同步;若条件不满足,则进行全量同步:主节点生成 RDB 并发送给副本,同时把同步期间的新写命令保存在复制缓冲区中;副本加载 RDB 后,再应用增量命令。
同步完成后,主节点持续把写命令流发送给副本。连接中断后,只要主节点复制积压缓冲区仍保留缺失的数据,副本就可以基于 offset 部分重同步;否则重新全量同步。Redis 复制默认是异步的,因此主节点确认写成功时,副本不一定已经收到。
42. Redis 主从复制的常见拓扑结构有哪些?
参考答案:
- 一主一从:结构简单,用于备份或读扩展。
- 一主多从:多个副本分担读流量,但全量同步会给主节点带来压力。
- 级联复制:副本继续挂载下级副本,降低主节点复制出口压力,但链路更长、延迟和运维复杂度更高。
- Sentinel 架构:主从复制加哨兵,实现监控和自动故障转移。
- Cluster 架构:多个主分片,每个主分片可带副本,同时实现分片和高可用。
从节点读存在延迟,不适合默认承载强一致读取。
43. Redis 复制延迟的常见原因有哪些?
参考答案:
- 主节点写入量过大,网络带宽不足。
- 主节点执行慢命令、Lua 长脚本或处理 Big Key,命令传播被阻塞。
- 从节点 CPU、磁盘或网络性能不足,应用复制流的速度跟不上。
- 全量同步生成和加载 RDB,或多个副本同时重同步。
- 主从跨地域、网络抖动、丢包或频繁断连。
- AOF 重写、RDB、内存换页等系统资源竞争。
- 副本输出缓冲区过小导致连接被关闭,进而反复重同步。
排查时关注主从 offset 差距、master_link_status、master_last_io_seconds_ago、复制缓冲区、网络和慢日志。强一致场景不能仅靠异步复制保证。
44. Redis 的哨兵机制是什么?
参考答案:
Sentinel 为非 Cluster 的主从架构提供:
- 监控:周期性检查主节点、副本及其他 Sentinel。
- 通知:报告实例故障或拓扑变化。
- 自动故障转移:主节点被判定下线后,选举一个副本晋升为新主节点,并让其他副本改为复制新主节点。
- 配置发现:客户端可通过 Sentinel 获取当前主节点地址。
单个 Sentinel 认为主节点不可达叫主观下线;达到配置的 quorum 后形成客观下线。之后 Sentinel 通过共识选出领导者执行故障转移。通常部署至少 3 个 Sentinel,并放在独立故障域。
45. Redis 集群的实现原理是什么?
参考答案:
Redis Cluster 将键空间划分为 16384 个哈希槽 。键通过 CRC16(key) mod 16384 映射到槽,槽再分配给不同主节点。客户端访问错误节点时,会收到 MOVED 或迁移过程中的 ASK 重定向;成熟客户端会缓存槽位拓扑。
节点之间通过 Cluster Bus 交换拓扑、心跳和故障信息。每个主节点可配置副本;主节点故障后,由其副本参与选举并接管槽。Cluster 同时解决分片和高可用,但不提供跨分片的全局强一致事务。
使用 {...} Hash Tag 可让多个键映射到同一槽,从而执行多键命令或 Lua 脚本,但也要防止单槽过热。
46. Redis Cluster 为什么使用 16384 个哈希槽?
参考答案:
16384 是 Redis Cluster 在元数据开销、心跳传播效率和可扩展性之间的工程折中。节点在 Gossip 心跳中会携带槽位位图;16384 个槽只需要 2048 字节,消息开销可控,同时槽数量足够多,便于在常见集群规模下均匀分配和迁移数据。
它不是由 CRC16 的位数直接决定的,也不是越多越好。Redis Cluster 预期的主节点规模有限,16384 已能提供足够的分片粒度;更多槽会增加集群消息和管理成本。
47. Redis Cluster 中 MOVED 和 ASK 有什么区别?
参考答案:
MOVED表示某个槽已经稳定归属于另一个节点。客户端应更新本地槽位映射,并把当前及后续请求发送到新节点。ASK表示槽正在迁移,当前这个键需要临时到目标节点访问。客户端不应永久修改槽位映射,而应先向目标节点发送ASKING,再重试本次命令。
成熟的 Cluster 客户端会自动处理两种重定向。频繁出现 MOVED 可能说明客户端槽缓存陈旧;大量 ASK 通常说明正在进行槽迁移。
48. Redis Cluster 模式与 Sentinel 模式的区别是什么?
参考答案:
| 对比项 | Sentinel | Cluster |
|---|---|---|
| 核心目标 | 主从高可用 | 数据分片 + 高可用 |
| 数据分布 | 一套主从保存完整数据 | 数据按 16384 个槽分散到多个主节点 |
| 扩容能力 | 主要受单主节点容量和写性能限制 | 可增加主分片并迁移槽 |
| 故障转移 | Sentinel 监控并提升副本 | 集群节点检测故障,由副本接管槽 |
| 客户端要求 | 支持 Sentinel 主节点发现 | 支持槽路由及 MOVED/ASK 重定向 |
| 多键操作 | 同一主节点内相对直接 | 多个键通常必须处于同一槽 |
数据量小、写入可由单主承担时 Sentinel 更简单;容量或写负载需要水平拆分时选择 Cluster。
49. 在 Redis 集群中,如何根据键定位到对应的节点?
参考答案:
- 计算键的槽位:
CRC16(key) mod 16384。 - 若键包含合法 Hash Tag,例如
order:{1001}:detail,只对第一个非空{1001}中的内容计算槽位。 - 根据客户端缓存的"槽---节点"映射找到目标节点。
- 若拓扑已变化,服务端返回
MOVED,客户端更新映射后重试;槽迁移中可能返回ASK,客户端先向目标节点发送ASKING再临时重试。
可以用 CLUSTER KEYSLOT key 查看键所在的槽。
50. Redis 集群会出现脑裂问题吗?
参考答案:
可能。网络分区时,旧主节点可能暂时仍接收客户端写入,而集群多数派一侧已把副本提升为新主节点;旧主恢复后会降为副本,其分区期间未复制的写入可能丢失。
降低风险的方法包括:
- 配置
min-replicas-to-write和min-replicas-max-lag,当主节点没有足够健康副本时拒绝写入。 - 让客户端及时刷新拓扑,并避免绕过集群感知直接固定连接旧主。
- 将主从部署在不同故障域,保证多数派判断可靠。
- 对不能容忍丢失的业务,在应用层增加幂等、日志或更强一致的数据系统。
这些措施只能缩小风险窗口;Redis 的异步复制不等于强一致共识。
四、事务、Lua、批处理与发布订阅
51. Redis 支持事务吗?如何实现?
参考答案:
支持。基本流程是 MULTI 开启事务、命令进入队列、EXEC 按顺序执行,或用 DISCARD 放弃。事务中的命令在执行阶段不会被其他客户端命令插入,因此具有隔离的串行执行效果。
但 Redis 事务不同于关系型数据库事务:它通常不提供回滚;若命令在入队时语法错误,整个事务可能拒绝执行;若执行时出现类型错误,其他命令仍继续执行。可以用 WATCH 监视键,实现乐观锁:被监视键在 EXEC 前发生变化时,事务执行失败并由客户端重试。
52. Redis 事务与关系型数据库事务的主要区别是什么?
参考答案:
- Redis 事务更像"命令排队后连续执行",关系型数据库事务通常提供完整 ACID 语义。
- Redis 不支持执行时错误的自动回滚;关系型数据库一般可以回滚。
- Redis 通过单线程串行执行保证事务命令不被插入;数据库依赖锁、MVCC 和隔离级别。
- Redis 的
WATCH是乐观并发控制;数据库可以提供多种锁和隔离级别。 - Redis 事务的持久性取决于 AOF/RDB 和刷盘策略,而不是
EXEC本身。 - Redis Cluster 中,多键事务通常要求相关键位于同一槽。
如果需要条件判断和原子修改,Lua 脚本往往比 WATCH 重试更直接。
53. Redis 的 Lua 脚本功能是什么?如何使用?
参考答案:
Lua 可以把多步读取、判断和写入放到服务器端作为一个整体执行,减少网络往返,并保证脚本执行期间不被其他命令插入。常用方式是 EVAL/EVALSHA;新版本也支持通过 Redis Functions 管理服务器端函数。
示例:仅当锁值等于当前客户端标识时删除锁:
lua
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
else
return 0
end
脚本应尽量短小,不能执行耗时循环或返回海量数据,否则会阻塞主线程。Cluster 中脚本涉及的键必须符合槽位约束,且键名应通过 KEYS 参数显式传入。
54. Redis 的 Pipeline 功能是什么?
参考答案:
Pipeline 允许客户端连续发送多条命令而不逐条等待响应,服务端依次执行后批量返回结果,主要作用是减少网络往返时间,提高吞吐量。
Pipeline 不保证事务原子性 ,命令之间仍可能穿插其他客户端命令;它也不同于服务端的 MSET 等原生命令。使用时要控制批次大小,否则会增加客户端和服务端输出缓冲区内存,并造成单批延迟过长。Redis Cluster 下还要按节点或槽位拆分 Pipeline。
55. Redis 中原生批处理命令(MSET、MGET)与 Pipeline 的区别是什么?
参考答案:
MSET、MGET是 Redis 原生命令,一次请求表达一个固定语义;单实例中MSET对多个键的写入是原子的。- Pipeline 是客户端优化机制,可组合任意多条命令,主要减少 RTT,但整批命令不是一个原子操作。
- 原生批量命令通常协议和执行开销更低;Pipeline 更灵活。
- Cluster 中无论原生多键命令还是需要同槽的原子操作,都受槽位限制;集群客户端可把可拆分的批量读请求按节点分组,但这不等于全局原子执行。
56. Redis 的订阅发布功能是什么?
参考答案:
Redis Pub/Sub 允许发布者向频道 PUBLISH 消息,订阅者使用 SUBSCRIBE 或模式订阅实时接收消息。发布者与订阅者解耦,适合在线通知、配置刷新等允许丢消息的场景。
它不做消息持久化,也没有消费确认和历史重放;订阅者离线期间的消息会丢失。需要可靠消费、消费者组和消息追踪时,应使用 Redis Stream 或专业消息队列。Redis Cluster 还提供 Sharded Pub/Sub,以减少消息在整个集群传播的开销。
五、分布式锁
57. Redis 中如何实现分布式锁?
参考答案:
单实例常用原子命令:
redis
SET lock:order:1001 <唯一请求标识> NX PX 30000
NX 保证不存在时才加锁,PX 设置过期时间避免死锁,值必须是当前持有者的唯一标识。释放时不能直接 DEL,应使用 Lua 原子地"比较标识后删除"。业务执行可能超过租期时,需要安全续期机制。
还要考虑:锁保护资源是否幂等、超时后旧持有者是否仍能写、主从切换是否造成锁丢失。严格正确性场景可引入 fencing token,或选择具备强一致共识的协调系统。
58. 分布式锁在未完成逻辑前过期怎么办?
参考答案:
- 预估业务最长时间并设置合理租期,同时避免持锁执行不可控的长任务。
- 使用看门狗定期续期,但续期前必须校验锁仍属于当前客户端;客户端停止或任务结束时停止续期。
- 释放锁时通过 Lua 比较唯一标识后删除,防止误删后来者的锁。
- 关键资源使用 fencing token:每次成功获取锁得到单调递增令牌,下游只接受更大的令牌,从而拒绝租约过期后旧持有者的迟到写入。
- 业务操作设计为幂等并保留状态校验。
仅靠自动续期无法解决进程长时间暂停、网络分区和旧持有者继续执行的全部问题。
59. Redis 的 Red Lock 是什么?
参考答案:
Redlock 是一种在多个相互独立 Redis 主节点上获取锁的算法。客户端以同一随机值、较短超时依次尝试在 N 个实例加锁;在锁有效期内获得多数实例成功才认为加锁成功,并用初始租期减去获取耗时和时钟漂移得到剩余有效时间。失败时释放已获得的锁。
它旨在降低单实例故障带来的锁丢失风险,但对网络分区、时钟、进程暂停和严格一致性的假设存在争议。若锁只用于降低重复任务概率可以评估使用;若锁直接保护资金、库存等强正确性资源,通常应使用基于共识的系统或增加 fencing token 和下游校验。
60. Redis 实现分布式锁时可能遇到的问题有哪些?
参考答案:
SETNX后再单独设置过期时间,进程在两步之间崩溃导致死锁。- 锁值不唯一或直接
DEL,误删其他客户端后来获得的锁。 - 业务执行超过租期,出现多个持有者同时工作。
- 主节点加锁后尚未复制就故障,副本提升后另一个客户端再次获得锁。
- 客户端 GC、系统暂停、网络分区导致锁已过期但旧任务仍继续执行。
- 续期线程异常,或续期没有验证所有权。
- 重入、公平性、惊群和高竞争导致吞吐下降。
- Cluster 多键锁未使用同一槽。
规范做法是原子加锁、唯一值、原子校验释放、合理租期与续期,再配合幂等和 fencing token;先明确业务需要"互斥优化"还是"强正确性保证"。
61. Redisson 看门狗(watchdog)机制了解吗?
参考答案:
Redisson 在未显式指定固定租期时,成功加锁后会启动看门狗,周期性检查持锁线程仍存活且仍持有该锁,然后延长锁的过期时间。默认看门狗超时时间通常为 30 秒,续期周期约为其三分之一,具体可通过配置调整。
Redisson 的锁值和 Lua 脚本用于维护所有权、重入次数及原子释放。当客户端崩溃或无法继续续期时,锁会在超时后自动释放。看门狗可以减少业务超时导致的锁提前失效,但不能消除网络分区、长时间停顿或主从异步复制带来的所有一致性风险。
六、缓存问题与一致性
62. Redis 中的缓存击穿、缓存穿透和缓存雪崩是什么?
参考答案:
- 缓存穿透:查询数据库中也不存在的数据,每次都绕过缓存打到数据库。方案:缓存空值、布隆过滤器、参数校验和限流。
- 缓存击穿:某个高频热点键失效瞬间,大量并发同时查询数据库并重建缓存。方案:互斥重建、逻辑过期、热点数据预热或不过期、请求合并。
- 缓存雪崩:大量键在相近时间失效,或 Redis 整体不可用,导致请求集中冲击数据库。方案:过期时间加随机抖动、多级缓存、高可用、限流熔断和降级。
三者都要结合数据库保护措施,不能只依赖缓存层解决。
63. Redis 中如何保证缓存与数据库的数据一致性?
参考答案:
最常见的是 Cache Aside:
- 读:先查缓存,未命中再查数据库并回填缓存。
- 写:先更新数据库,成功后删除缓存,而不是同时更新缓存。
"先更新数据库再删缓存"仍存在短暂不一致窗口,可按业务要求增加:
- 延迟双删,降低旧值被并发读回填的概率。
- 通过事务消息、Outbox、CDC/Canal 订阅数据库变更,可靠地删除或更新缓存。
- 给缓存设置 TTL 作为最终兜底。
- 使用版本号或时间戳,避免旧事件覆盖新值。
- 对强一致读绕过缓存,或把关键写读放到同一权威数据源。
不存在对所有场景都零成本的完美方案,应先确定允许的不一致时间和失败补偿机制。
64. 如何使用 Redis 快速实现布隆过滤器?
参考答案:
优先使用 RedisBloom 模块:
redis
BF.RESERVE user:bf 0.01 1000000
BF.ADD user:bf 1001
BF.EXISTS user:bf 1001
也可以用 Bitmap 自行实现:对元素计算多组哈希值,将对应位通过 SETBIT 置 1;查询时用相同哈希检查所有位。布隆过滤器可能误判"存在",但不会把已加入元素误判为"不存在"(不考虑删除和实现错误)。容量、哈希函数数量和误判率必须预先估算;普通布隆过滤器不支持安全删除。
典型用途是拦截缓存穿透。它只能说明"可能存在",命中后仍要查缓存或数据库。
65. 如何使用 Redis 统计大量用户唯一访问量(UV)?
参考答案:
使用 HyperLogLog:
redis
PFADD uv:2026-09-01 user:1001 user:1002
PFCOUNT uv:2026-09-01
它以固定且较小的内存估算基数,标准误差约 0.81%,非常适合大规模 UV。多个统计可用 PFMERGE 合并。缺点是结果不是精确值,也不能列出具体用户。
需要精确统计且用户 ID 范围紧凑时可使用 Bitmap;要保存明细可使用 Set,但内存成本更高。
66. 如何使用 Redis 快速实现排行榜?
参考答案:
使用 ZSet:成员保存用户 ID,score 保存积分。
redis
ZADD rank:game 100 user:1
ZINCRBY rank:game 20 user:1
ZREVRANGE rank:game 0 9 WITHSCORES
ZREVRANK rank:game user:1
如果分数相同还需要按时间等规则排序,可把多个维度编码进 score,但必须注意 IEEE 754 双精度整数的精确范围;也可以通过二级排序逻辑或额外结构处理。周期榜单可按日期拆键并设置 TTL,发榜时保留快照。
七、客户端、内部机制与实战
67. 你在项目中使用的 Redis 客户端是什么?
参考答案(应结合自己的项目修改):
Java 项目常见回答:使用 Redisson 实现分布式锁和高级对象,使用 Lettuce(Spring Data Redis 默认常见客户端)处理常规缓存访问。Lettuce 基于 Netty,连接线程安全,支持异步、响应式和 Cluster;Jedis API 直接,但连接通常不是线程安全的,需要连接池管理。
面试时不要只报名称,还应说明:连接池或连接复用配置、序列化方式、超时与重试策略、Cluster/Sentinel 支持,以及在项目中遇到的连接泄漏、超时或热 Key 问题。
68. Redis 客户端连接池应该如何配置?
参考答案:
连接池没有通用固定值,应根据实例处理能力、应用实例数、并发量和命令耗时估算。重点配置最大连接数、最小空闲连接、获取连接超时、连接和命令超时、空闲检测及连接泄漏监控。
连接数不是越多越好:所有应用实例的连接总数不能超过 Redis 承载能力,过多连接会增加文件描述符、内存和调度开销。还应避免在每次请求中创建新连接,区分普通连接、阻塞命令和 Pub/Sub 专用连接。重试必须有次数上限、退避和幂等保证,不能在 Redis 变慢时制造重试风暴。
69. Redis 源码中有哪些巧妙的设计?举几个典型例子。
参考答案:
- 渐进式 Rehash:哈希表扩缩容不是一次完成,而是把迁移工作分摊到后续操作和定时任务中,减少长时间阻塞。
- SDS:O(1) 长度、二进制安全,并按长度使用不同头部以节省内存。
- 紧凑编码:小对象使用 ListPack 等连续内存结构,达到阈值后再转换,平衡内存与性能。
- 跳表 + 哈希表:ZSet 同时兼顾成员查分数、排序、排名和范围查询。
- 过期处理:惰性删除与定期抽样组合,避免维护海量定时器。
- 事件循环:基于 I/O 多路复用处理大量连接,并通过时间事件执行周期任务。
- 写时复制:RDB/AOF 重写利用 fork 后的 COW,在服务请求的同时生成一致快照。
- Lazy Free:把某些大对象的内存释放放到后台线程,降低主线程停顿。
70. Redis 的虚拟内存(VM)机制是什么?
参考答案:
Redis 早期版本曾提供 VM 机制:当内存不足时,把较少访问的 value 换到磁盘,而 key 和必要元数据保留在内存;访问时再从磁盘加载。由于实现复杂、随机磁盘 I/O 影响延迟,而且与 Redis 的内存数据库定位不匹配,该机制后来被移除,现代 Redis 不再支持。
现在应通过设置 maxmemory 和淘汰策略、拆分实例、Cluster 水平扩容、冷热分层或使用其他磁盘型存储解决容量问题。操作系统 swap 也应尽量避免,因为它会造成不可预测的延迟。
71. Redis 和 Memcached 有哪些区别?
参考答案:
| 对比项 | Redis | Memcached |
|---|---|---|
| 数据结构 | 支持 String、Hash、List、Set、ZSet、Stream 等 | 主要是简单的键值缓存 |
| 持久化 | 支持 RDB、AOF 及混合持久化 | 通常不提供持久化,重启后缓存丢失 |
| 高可用与分片 | 原生支持复制、Sentinel、Cluster | 通常依赖客户端一致性哈希或外部代理分片 |
| 高级能力 | 事务、Lua、Pub/Sub、过期策略、分布式锁等 | 能力更聚焦于简单缓存 |
| 线程模型 | 命令执行主要为单线程,网络 I/O 可多线程 | 多线程处理请求 |
| 内存管理 | 数据结构丰富,元数据和持久化会带来额外开销 | 简单 KV 场景下模型更轻量 |
如果只需要简单、易扩展的临时 KV 缓存,Memcached 依然合适;若需要丰富数据结构、持久化、高可用或原子操作,Redis 通常更合适。选型不能只比较单次基准性能,还要考虑业务能力和运维成本。
72. Redisson 分布式锁的原理是什么?
参考答案:
Redisson 的可重入锁通常使用 Redis Hash 保存锁状态:字段标识客户端和线程,值记录重入次数。加锁通过 Lua 脚本原子完成:锁不存在则创建并设置 TTL;如果当前线程已持有则增加重入次数并续期;否则返回剩余 TTL。未获得锁的客户端可结合 Pub/Sub 通知与等待机制重试,减少纯轮询。
解锁同样使用 Lua:验证持有者,重入次数减一;降到零时删除锁并发布解锁消息。未指定固定租期时,看门狗会自动续期。Redisson 还提供公平锁、读写锁、联锁等实现,但它底层仍受 Redis 租约、异步复制和网络分区语义约束,关键业务应配合幂等及 fencing token。
73. Redis 如何帮助实现接口幂等?
参考答案:
客户端或服务端为一次业务操作生成唯一请求号,服务端使用 SET idempotent:<请求号> 处理中 NX EX <超时> 抢占处理权;只有设置成功的请求继续执行业务,重复请求直接返回已有结果或"处理中"。业务完成后可以把状态和结果更新为"成功",并保留合理 TTL。
需要注意,Redis 标记与数据库提交不是天然原子操作。如果业务提交成功但状态更新失败,仍可能重复执行,因此关键操作还应使用数据库唯一约束、状态机、事务消息或 Outbox 做最终兜底。幂等记录的 TTL 必须覆盖可能的重试周期,不能只依赖一把短期分布式锁。
面试复习建议
回答 Redis 题目时,可以使用统一结构:先给结论,再讲底层实现,最后说明适用场景和风险 。例如回答分布式锁时,不只说 SET NX PX,还要补充唯一值、Lua 解锁、续期、主从切换和 fencing token;回答缓存一致性时,则要说明异常场景、补偿机制和业务允许的不一致窗口。