这是《Redis 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 本章按“基础 -> 数据结构 -> 原理 -> 高可用 -> 分布式锁 -> 缓存架构 -> 性能运维 -> 场景设计”组织 50 道高频题。建议先遮住答案自己回答,再对照补充。
28.1 基础篇
Q1. Redis 是什么?有哪些典型用途?
Redis 是基于内存的键值数据库,支持 String、Hash、List、Set、ZSet、Stream 等结构,并提供持久化、复制、哨兵和 Cluster。典型用途包括缓存、会话、计数器、排行榜、分布式锁、限流、轻量队列和实时状态存储。
Q2. Redis 为什么快?
核心原因:数据在内存、高效数据结构、单线程命令执行避免锁竞争、IO 多路复用、简单协议 RESP、渐进式复杂度设计和 pipeline 批量传输。Redis 6 后网络 IO 可以多线程处理,但命令执行仍是主线程串行,保证单个节点上的命令原子性。
Q3. Redis 是单线程吗?
不准确。命令执行通常由主线程完成,但后台线程负责 RDB、AOF 刷盘、异步删除等任务;Redis 6 引入 IO 多线程负责读写网络协议。面试时可以强调“命令执行是单线程,进程不是单线程”。
Q4. Redis 中 DB 是物理隔离吗?
不是。不同 DB 只是同一个实例内的命名空间,共享内存、CPU、持久化、网络和淘汰策略。生产上核心与非核心业务应使用不同实例,而不是只依赖 SELECT。
Q5. RESP 是什么?
RESP 是 Redis 序列化协议,客户端与服务端通过简单文本格式交互。RESP2 以 +、-、:、$、* 标识简单字符串、错误、整数、批量字符串和数组;RESP3 增加 map、double、bool、push 等类型。
Q6. 缓存和数据库的本质区别是什么?
缓存追求低延迟和吞吐,数据通常可丢、可重建;数据库追求完整性、一致性、事务和审计。缓存可以是数据库前面的投影,但不应替代事实源。
Q7. Redis 支持事务吗?和 MySQL 事务有什么区别?
Redis MULTI/EXEC 能把命令排队并顺序执行,期间不会被其他客户端命令插入,但执行中某条命令失败后不会回滚已执行命令。它更接近批量原子执行,不提供 MySQL 那样的完整隔离级别和回滚语义。
Q8. WATCH 的作用是什么?
WATCH 实现 CAS。事务执行前如果被 watch 的 key 发生变化,EXEC 返回 nil,事务放弃。适合秒杀扣减这类基于版本检查的小并发逻辑,复杂场景通常用 Lua 或数据库条件更新。
Q9. Lua 脚本原子吗?有什么风险?
Lua 脚本在单个 Redis 节点执行期间不会被其他命令插入,因此脚本内多命令原子。风险是脚本慢会阻塞节点,网络分区和主从切换仍可能带来业务层重复执行,脚本必须幂等且执行时间可控。
Q10. Redis 常见阻塞点有哪些?
大 key 读写、KEYS、SMEMBERS 全量集合、大范围 ZRANGE、HGETALL、复杂 Lua、SINTERSTORE 类聚合命令、AOF fsync 策略、RDB fork、交换内存、网络重传、大量连接和磁盘 IO 抖动。
28.2 数据结构篇
Q11. 五大基础类型的适用场景是什么?
String 存储缓存、计数器、分布式锁;Hash 存对象字段;List 存简单队列和最新列表;Set 存去重和共同关注;ZSet 存排行榜、延迟队列和时间线。
Q12. String 存对象和 Hash 存对象怎么选?
String JSON 适合整体读写、序列化后传输、字段更新少;Hash 适合局部字段更新、字段级 TTL 需求较少、需要读取部分字段。若 value 很大或字段很多,要拆分或压缩。
Q13. SDS 相比 C 字符串有什么优势?
SDS 保存长度,获取长度 O(1);支持动态扩容和空间预分配;二进制安全;可通过空闲空间减少内存重分配;部分类型支持惰性空间释放。
Q14. ZSet 为什么同时使用跳表和字典?
字典支持 member 到 score 的 O(1) 查找;跳表支持按 score 排序和范围查询 O(log N)。两者结合让单点查询和排序查询都快,代价是额外内存。
Q15. 为什么 ZSet 使用跳表而不是红黑树?
跳表实现更简单,范围查询和节点遍历方便,并可通过多层链表实现近似平衡。红黑树也能做到 O(log N),但实现和迭代复杂,Redis 选择了工程上更容易维护的结构。
Q16. List 的底层编码是什么?
旧版本主要使用 ziplist 和 linkedlist;Redis 7 引入 listpack 替代 ziplist 的一部分场景。小列表使用紧凑编码节省内存,变大后转换标准结构。
Q17. Hash 的小对象编码是什么?为什么省内存?
小 Hash 可使用 listpack/ziplist,字段连续存储并保留少量元数据,减少指针和单独分配开销。元素过多或过大后转为 hashtable,避免紧凑结构更新成本过高。
Q18. Bitmap 适合什么场景?
签到、活跃用户、权限位、布尔状态。存储 N 天状态约 N/8 字节。适合稠密整数 ID,如果 ID 稀疏会浪费内存,需要映射或换 Roaring Bitmap。
Q19. HyperLogLog 的原理和误差是多少?
HyperLogLog 通过哈希值前导零的最大值估算基数,标准误差约 0.81%。它内存极小,但不保存元素,也不能取出成员,适合 UV 这类近似去重计数。
Q20. GEO 的底层原理是什么?
Redis GEO 基于 ZSet,将经纬度编码为 52 bit 的 geohash 作为 score。支持范围查询和距离计算,适合附近的人、附近门店等中小规模场景;复杂地理分析可用专业 GIS 或搜索引擎。
28.3 内存与过期删除篇
Q21. Redis 如何删除过期 key?
结合惰性删除和定期删除。访问 key 时检查过期则删除;后台周期性随机采样过期字典并删除。Redis 6 后还支持异步释放内存,避免删除大 key 阻塞主线程。
Q22. 定期删除为什么要限制时间?
过期 key 很多时,如果一直删除会占用 CPU 并阻塞正常命令。Redis 通过采样数量、删除比例和时间上限做自适应,把清理任务摊开执行。
Q23. maxmemory 达到后会发生什么?
写入命令根据 maxmemory-policy 决定是否拒绝或淘汰 key。默认行为可能是 noeviction,写命令报错;也可选 LRU、LFU、random、TTL 相关策略。
Q24. LRU 和 LFU 的区别?
LRU 淘汰最近最少使用,可能被偶发扫描污染;LFU 淘汰访问频率低的 key,更适合长期热点。Redis 使用抽样近似 LRU/LFU,而不是全局精确双向链表。
Q25. allkeys-lru 和 volatile-lru 怎么选?
纯缓存实例可用 allkeys-lru,所有 key 都可淘汰;实例中混有锁、队列、状态等不可丢数据时不能用 allkeys 策略,应拆实例或谨慎设置 volatile-* 与 noeviction。
Q26. 什么是内存碎片?如何观察和处理?
碎片来自分配器空洞和大量不同大小对象的创建删除。观察 INFO memory 的 mem_fragmentation_ratio。适度升高可接受,过高可分析大 key 和淘汰,重启或使用 activedefrag 需评估负载和版本。
Q27. 大 key 的危害和处理方式?
大 key 导致网络阻塞、序列化慢、内存集中、删除阻塞、迁移慢。处理方式是拆分结构、压缩、分页、缩短 TTL、使用 SCAN 定位、异步删除和治理业务模型。
Q28. 热点 key 的危害和处理方式?
热点 key 使 Cluster 单分片 CPU、带宽或 QPS 过高。处理方式是本地缓存、key 副本拆分、读写分离、限流、静态化、CDN 和业务错峰。
28.4 持久化与高可用篇
Q29. RDB 和 AOF 的区别?
RDB 是某个时刻的二进制快照,文件小、恢复快,但可能丢失上次快照后的数据;AOF 记录写命令,丢失窗口取决于刷盘策略,文件更大、恢复更慢。Redis 7 使用 multipart AOF,base 和 incr 文件分离。
Q30. AOF 三种刷盘策略是什么?
always 每条命令刷盘,最可靠但性能最低;everysec 每秒刷盘,通常推荐;no 交给操作系统,性能最好但丢失窗口更大。
Q31. AOF 重写为什么存在?
AOF 会记录多次修改同一 key 的历史命令,文件越来越大。重写根据当前内存状态生成等价的最小命令集,降低文件体积和恢复时间。重写由子进程执行,期间新写入进入重写缓冲区。
Q32. 混合持久化是什么?
AOF 重写文件前半部分使用 RDB 格式,之后的增量使用 AOF 命令。恢复速度和数据完整性取得平衡,Redis 7 中通常通过 aof-use-rdb-preamble 控制。
Q33. fork 子进程时为什么会影响主进程?
fork 需要复制页表,实例内存很大时耗时增加。写时复制期间如果页被大量修改,会额外消耗内存,还可能造成 latency 和内存压力。
Q34. 主从复制的流程是什么?
从节点发送 PSYNC;无法增量同步时执行全量同步,主节点 bgsave 生成 RDB 并发送,同时积累写缓冲区;从节点加载 RDB 后继续应用增量命令。断线后可根据 replid 和 backlog 尝试部分重同步。
Q35. replid 和 replication backlog 的作用?
主节点有 replid 和复制偏移量,backlog 保存最近的写命令。从节点携带自己的 replid 和 offset,若仍匹配且 offset 在 backlog 范围内,可只补增量,否则全量同步。
Q36. 哨兵如何判断主节点故障?
单个哨兵 ping 超时认为主观下线;达到 quorum 个哨兵同意后升级为客观下线;哨兵之间选举 leader 执行故障转移。down-after-milliseconds 和 quorum 要根据网络状况设置。
Q37. 哨兵故障转移选择从节点的规则是什么?
会过滤不健康或长时间断连的节点,再根据优先级、复制偏移量和 runid 排序。优先级小者优先;偏移量大者优先;最后比较 runid。执行转移时会升级从节点并让其他从节点复制新主。
Q38. Redis Cluster 如何分片?
共有 16384 个 slot,CRC16(key) mod 16384 计算 key 所属槽。节点负责部分槽,客户端可按 slot 映射直连目标节点。hash tag 可以强制相关 key 落在同一槽。
Q39. MOVED 和 ASK 的区别?
MOVED 表示槽已经归属新节点,客户端应更新 slot 映射;ASK 表示槽正在迁移,当前请求可先向目标节点发送 ASKING 再执行,属于临时重定向。
Q40. Cluster 模式有哪些限制?
多 key 命令、事务和 Lua 通常要求 key 在同一槽;数据库只有 DB0;客户端需要感知集群拓扑;reshard 期间有迁移复杂度;节点太少无法满足高可用副本分布要求。
28.5 分布式锁与缓存篇
Q41. Redis 分布式锁怎么实现?
使用 SET key uniqueValue NX PX ttl 加锁,业务结束后通过 Lua 比较 value 并删除,避免误删别人的锁。执行时间可能超过 TTL 时使用 Redisson watchdog 续期,同时业务必须有幂等兜底。
Q42. 为什么要保存唯一 value?
如果客户端 A 过期后释放锁,客户端 B 已获取锁,A 直接 DEL 会把 B 的锁删掉。Lua 脚本先比较唯一 value 再删除,可以降低误删概率。
Q43. Redlock 适合什么场景?
Redlock 在多个独立 Redis 节点上加锁,多数成功并在有效期内才算成功。它试图减少单实例故障影响,但不能解决时钟跳变、长时间 GC、网络分区下的所有安全性问题。多数业务用单主锁加业务幂等更务实。
Q44. 缓存穿透是什么?如何防护?
请求不存在的数据,缓存和数据库都无法命中。防护包括参数校验、空值缓存、布隆过滤器、风控限流和恶意请求拦截。空值 TTL 应较短。
Q45. 缓存击穿和雪崩的区别?
击穿是单个热点 key 过期瞬间大量请求回源;雪崩是大量 key 同时失效或 Redis 整体不可用导致数据库压力骤增。击穿用互斥加载、逻辑过期和热点不过期;雪崩用 TTL 抖动、多级缓存、限流降级和高可用架构。
Q46. 先更新数据库再删缓存是否保证一致?
不保证强一致,只是出错概率较低的通用方案。仍存在读请求未命中后加载旧值、删除失败、主从延迟等问题。需要 TTL 兜底、失败重试、binlog 失效或版本号校验。
Q47. 延迟双删的适用边界?
延迟双删用于降低“先删缓存后更新库”或读写并发造成旧值回填的概率。延迟时间难以精确选择,且不能保证强一致。适合秒级最终一致业务,不适合账务等强一致场景。
Q48. 如何设计缓存降级?
缓存故障时按业务重要性处理:本地缓存返回旧值、返回静态兜底数据、非核心字段隐藏、核心接口限流排队,数据库回源必须有限流。降级策略要预先配置并演练。
28.6 性能运维与场景设计篇
Q49. Redis 延迟突然升高,如何排查?
按顺序看:慢日志、latency monitor、命令统计、大 key、热点 key、客户端连接和超时、网络重传、CPU、内存和 swap、RDB/AOF 活动、主从复制状态、集群迁移和故障切换记录。不要一上来就重启。
Q50. 设计一个秒杀库存系统,你会怎么做?
分层防护:前端静态化和按钮控制;网关限流和黑名单;应用校验活动状态与资格;Redis Lua 原子完成重复判断和库存扣减;Kafka 异步创建订单;数据库唯一键和条件库存更新兜底;订单超时回补库存;活动后对账 Redis、消息和数据库;全链路监控和降级预案。
28.7 回答框架建议
面试中回答 Redis 问题可以使用四步:
结论:先给出明确答案
原理:说明 Redis 内部机制或时序
边界:指出不适用场景或失败窗口
实践:给出生产上的防护和监控
例如回答持久化时,不要只说“RDB 快、AOF 不丢”,而要补充:RDB 有快照间隔的丢失窗口,AOF everysec 也可能丢一秒;生产要根据 RPO 选择策略,并用备份和故障演练验证恢复。
28.8 本章小结
- 基础问题要准确,例如“命令执行单线程”不等于进程单线程;
- 数据结构问题要落到编码、复杂度和业务场景;
- 持久化与高可用问题必须说明丢失窗口和故障切换影响;
- 分布式锁不能只讲命令,要讲误删、续期、分区和业务幂等;
- 缓存问题要区分穿透、击穿、雪崩,并说明一致性等级;
- 性能和场景题要展示排查顺序、边界条件和兜底方案。
28.9 思考题
- 你所在业务中哪些 Redis 数据是可丢的?哪些是不可丢的?分别如何验证?
- 如果面试官追问“Redis 能不能保证分布式锁绝对安全”,你的最终答案是什么?
- 如何向架构师解释“缓存强一致很难,但可以做可审计的最终一致”?
- 生产实例 CPU 100% 且命令 QPS 不高,你会优先排查哪些原因?
- 设计一个 10 万 QPS 的用户资料缓存,你会输出哪些容量、命中率和降级指标?