一、知识点总览
1.1 Redis 官方定义与核心定位
Redis(REmote DIctionary Server) 是一款开源、基于内存运行、支持持久化的高性能 Key-Value 型 NoSQL 数据库,使用 ANSI C 语言编写,遵循 BSD 协议。其官方定义为:
"Redis is an open source (BSD licensed), in-memory data structure store, used as a database, cache, message broker, and streaming engine."
即 Redis 是一个内存数据结构存储系统,可同时承担四类角色:
| 角色 | 说明 | 典型场景 |
|---|---|---|
| 数据库 | 支持持久化、事务、多数据结构 | 轻量级业务主数据存储、会话存储 |
| 缓存 | 亚毫秒级读写,降低后端数据库压力 | 热点数据缓存、接口结果缓存 |
| 消息中间件 | Pub/Sub、Stream 数据结构 | 轻量消息通知、日志流处理 |
| 流处理引擎 | Stream 支持消费者组与 ACK 机制 | 事件溯源、订单流水处理 |
核心特性:
- 极致性能:单机读写 QPS 可达 10 万+,读操作平均耗时微秒级;
- 丰富数据结构:String、Hash、List、Set、ZSet 五大基础类型,外加 Bitmap、HyperLogLog、GEO、Stream 等扩展类型;
- 持久化能力:RDB 快照 + AOF 命令日志,支持混合持久化;
- 高可用架构:主从复制、Sentinel 哨兵、Cluster 集群三级演进;
- 原子性保证:单线程命令处理模型 + Lua 脚本,天然规避并发竞争;
- 功能丰富:事务、发布订阅、分布式锁、限流、布隆过滤器(RedisBloom 模块)等。
1.2 Redis 为什么快(核心原因总览)
这是面试最高频的总览性问题,标准答案包含五个层面:
- 纯内存操作:数据存储在内存中,读写延迟纳秒级,无磁盘 I/O 瓶颈;
- 单线程命令处理模型(Redis 6.0 前):无锁、无上下文切换、无竞态条件,命令执行天然原子;
- I/O 多路复用:基于 epoll/kqueue 的事件驱动模型(ae 事件库),单线程高效处理海量连接;
- 高效底层数据结构:SDS、跳表、压缩列表、哈希表等针对性优化的结构;
- 6.0 引入多线程 I/O:网络读写与协议解析多线程化,命令执行仍单线程,进一步提升网络吞吐。
1.3 知识点可视化图例
(1)Redis 整体架构图
#mermaid-svg-wlb5KoxdpJLCU4Vk{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-wlb5KoxdpJLCU4Vk .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wlb5KoxdpJLCU4Vk .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wlb5KoxdpJLCU4Vk .error-icon{fill:#552222;}#mermaid-svg-wlb5KoxdpJLCU4Vk .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wlb5KoxdpJLCU4Vk .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wlb5KoxdpJLCU4Vk .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wlb5KoxdpJLCU4Vk .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wlb5KoxdpJLCU4Vk .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wlb5KoxdpJLCU4Vk .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wlb5KoxdpJLCU4Vk .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wlb5KoxdpJLCU4Vk .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wlb5KoxdpJLCU4Vk .marker.cross{stroke:#333333;}#mermaid-svg-wlb5KoxdpJLCU4Vk svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wlb5KoxdpJLCU4Vk p{margin:0;}#mermaid-svg-wlb5KoxdpJLCU4Vk .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-wlb5KoxdpJLCU4Vk .cluster-label text{fill:#333;}#mermaid-svg-wlb5KoxdpJLCU4Vk .cluster-label span{color:#333;}#mermaid-svg-wlb5KoxdpJLCU4Vk .cluster-label span p{background-color:transparent;}#mermaid-svg-wlb5KoxdpJLCU4Vk .label text,#mermaid-svg-wlb5KoxdpJLCU4Vk span{fill:#333;color:#333;}#mermaid-svg-wlb5KoxdpJLCU4Vk .node rect,#mermaid-svg-wlb5KoxdpJLCU4Vk .node circle,#mermaid-svg-wlb5KoxdpJLCU4Vk .node ellipse,#mermaid-svg-wlb5KoxdpJLCU4Vk .node polygon,#mermaid-svg-wlb5KoxdpJLCU4Vk .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-wlb5KoxdpJLCU4Vk .rough-node .label text,#mermaid-svg-wlb5KoxdpJLCU4Vk .node .label text,#mermaid-svg-wlb5KoxdpJLCU4Vk .image-shape .label,#mermaid-svg-wlb5KoxdpJLCU4Vk .icon-shape .label{text-anchor:middle;}#mermaid-svg-wlb5KoxdpJLCU4Vk .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-wlb5KoxdpJLCU4Vk .rough-node .label,#mermaid-svg-wlb5KoxdpJLCU4Vk .node .label,#mermaid-svg-wlb5KoxdpJLCU4Vk .image-shape .label,#mermaid-svg-wlb5KoxdpJLCU4Vk .icon-shape .label{text-align:center;}#mermaid-svg-wlb5KoxdpJLCU4Vk .node.clickable{cursor:pointer;}#mermaid-svg-wlb5KoxdpJLCU4Vk .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-wlb5KoxdpJLCU4Vk .arrowheadPath{fill:#333333;}#mermaid-svg-wlb5KoxdpJLCU4Vk .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-wlb5KoxdpJLCU4Vk .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-wlb5KoxdpJLCU4Vk .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wlb5KoxdpJLCU4Vk .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-wlb5KoxdpJLCU4Vk .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wlb5KoxdpJLCU4Vk .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-wlb5KoxdpJLCU4Vk .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-wlb5KoxdpJLCU4Vk .cluster text{fill:#333;}#mermaid-svg-wlb5KoxdpJLCU4Vk .cluster span{color:#333;}#mermaid-svg-wlb5KoxdpJLCU4Vk div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-wlb5KoxdpJLCU4Vk .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-wlb5KoxdpJLCU4Vk rect.text{fill:none;stroke-width:0;}#mermaid-svg-wlb5KoxdpJLCU4Vk .icon-shape,#mermaid-svg-wlb5KoxdpJLCU4Vk .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wlb5KoxdpJLCU4Vk .icon-shape p,#mermaid-svg-wlb5KoxdpJLCU4Vk .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-wlb5KoxdpJLCU4Vk .icon-shape .label rect,#mermaid-svg-wlb5KoxdpJLCU4Vk .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wlb5KoxdpJLCU4Vk .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-wlb5KoxdpJLCU4Vk .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-wlb5KoxdpJLCU4Vk :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 高可用层
持久化层
Redis Server 服务端
客户端层
TCP + RESP 协议
fork 子进程
追加写入
Java / Go / Python 客户端
Jedis · Lettuce · go-redis
RESP 协议解析层
命令分发与执行层
单线程事件循环 aeEventLoop
数据结构实现层
SDS / dict / skiplist / listpack / intset
内存管理层
zmalloc + jemalloc
RDB 快照文件
AOF 命令日志
主从复制 Replication
哨兵 Sentinel
Cluster 集群
图注:客户端命令经 RESP 协议解析后进入单线程事件循环,命令在数据结构层落地执行;持久化与高可用能力以"旁路"形式挂载,不影响主链路低延迟。
(2)一条命令的完整执行流程图
#mermaid-svg-7KLUQ6cNuLFTuqCq{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-7KLUQ6cNuLFTuqCq .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-7KLUQ6cNuLFTuqCq .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-7KLUQ6cNuLFTuqCq .error-icon{fill:#552222;}#mermaid-svg-7KLUQ6cNuLFTuqCq .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-7KLUQ6cNuLFTuqCq .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-7KLUQ6cNuLFTuqCq .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-7KLUQ6cNuLFTuqCq .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-7KLUQ6cNuLFTuqCq .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-7KLUQ6cNuLFTuqCq .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-7KLUQ6cNuLFTuqCq .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-7KLUQ6cNuLFTuqCq .marker{fill:#333333;stroke:#333333;}#mermaid-svg-7KLUQ6cNuLFTuqCq .marker.cross{stroke:#333333;}#mermaid-svg-7KLUQ6cNuLFTuqCq svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-7KLUQ6cNuLFTuqCq p{margin:0;}#mermaid-svg-7KLUQ6cNuLFTuqCq .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-7KLUQ6cNuLFTuqCq .cluster-label text{fill:#333;}#mermaid-svg-7KLUQ6cNuLFTuqCq .cluster-label span{color:#333;}#mermaid-svg-7KLUQ6cNuLFTuqCq .cluster-label span p{background-color:transparent;}#mermaid-svg-7KLUQ6cNuLFTuqCq .label text,#mermaid-svg-7KLUQ6cNuLFTuqCq span{fill:#333;color:#333;}#mermaid-svg-7KLUQ6cNuLFTuqCq .node rect,#mermaid-svg-7KLUQ6cNuLFTuqCq .node circle,#mermaid-svg-7KLUQ6cNuLFTuqCq .node ellipse,#mermaid-svg-7KLUQ6cNuLFTuqCq .node polygon,#mermaid-svg-7KLUQ6cNuLFTuqCq .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-7KLUQ6cNuLFTuqCq .rough-node .label text,#mermaid-svg-7KLUQ6cNuLFTuqCq .node .label text,#mermaid-svg-7KLUQ6cNuLFTuqCq .image-shape .label,#mermaid-svg-7KLUQ6cNuLFTuqCq .icon-shape .label{text-anchor:middle;}#mermaid-svg-7KLUQ6cNuLFTuqCq .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-7KLUQ6cNuLFTuqCq .rough-node .label,#mermaid-svg-7KLUQ6cNuLFTuqCq .node .label,#mermaid-svg-7KLUQ6cNuLFTuqCq .image-shape .label,#mermaid-svg-7KLUQ6cNuLFTuqCq .icon-shape .label{text-align:center;}#mermaid-svg-7KLUQ6cNuLFTuqCq .node.clickable{cursor:pointer;}#mermaid-svg-7KLUQ6cNuLFTuqCq .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-7KLUQ6cNuLFTuqCq .arrowheadPath{fill:#333333;}#mermaid-svg-7KLUQ6cNuLFTuqCq .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-7KLUQ6cNuLFTuqCq .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-7KLUQ6cNuLFTuqCq .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7KLUQ6cNuLFTuqCq .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-7KLUQ6cNuLFTuqCq .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7KLUQ6cNuLFTuqCq .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-7KLUQ6cNuLFTuqCq .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-7KLUQ6cNuLFTuqCq .cluster text{fill:#333;}#mermaid-svg-7KLUQ6cNuLFTuqCq .cluster span{color:#333;}#mermaid-svg-7KLUQ6cNuLFTuqCq div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-7KLUQ6cNuLFTuqCq .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-7KLUQ6cNuLFTuqCq rect.text{fill:none;stroke-width:0;}#mermaid-svg-7KLUQ6cNuLFTuqCq .icon-shape,#mermaid-svg-7KLUQ6cNuLFTuqCq .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7KLUQ6cNuLFTuqCq .icon-shape p,#mermaid-svg-7KLUQ6cNuLFTuqCq .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-7KLUQ6cNuLFTuqCq .icon-shape .label rect,#mermaid-svg-7KLUQ6cNuLFTuqCq .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7KLUQ6cNuLFTuqCq .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-7KLUQ6cNuLFTuqCq .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-7KLUQ6cNuLFTuqCq :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 内存超限
校验通过
客户端发送命令
socket 可读事件触发
读取请求数据 readQueryFromClient
RESP 协议解析为 argv
命令表查找 processCommand
前置校验
执行内存淘汰策略
call 执行命令
更新内存数据结构
传播命令: AOF 缓冲 / 从库复制缓冲
写回响应 addReply
图注:理解"执行 → 传播 → 响应"三段式链路,是掌握持久化与主从复制原理的基础。
1.4 同类型技术横向对比
选取与 Redis 功能、用途、场景高度相似的 Memcached 与阿里 Tair 进行六维度对标:
| 对比维度 | Redis | Memcached | Tair(阿里) |
|---|---|---|---|
| 核心定位 | 内存数据结构服务器,兼顾持久化与高可用 | 纯内存 KV 缓存,极简设计 | 分布式 KV 存储,支持多引擎(MDB/LDB/RDB) |
| 技术特性 | 五大+扩展数据类型、事务、Lua、Pub/Sub、Stream、RDB/AOF 持久化 | 仅 String 类型、无持久化、多线程模型 | 多引擎可选、自动数据迁移、双机房容灾 |
| 性能优劣 | 单线程模型,单核性能强;6.0 多线程 I/O 后网络吞吐提升;复杂数据结构操作耗时略高 | 多线程充分利用多核,简单 KV 场景吞吐略优 | 原生分布式,水平扩展能力强,运维成本高 |
| 适用场景 | 缓存、排行榜、分布式锁、会话、计数、消息流等复合场景 | 简单 KV 缓存、页面片段缓存 | 超大规模、需要自动分片与多副本的企业级场景 |
| 局限性 | 单实例内存受限,Cluster 对多键事务/Lua 有 slot 约束 | 无持久化、无数据类型、无原生高可用方案 | 社区生态弱,深度绑定阿里云体系 |
| 选型优先级 | 绝大多数互联网业务首选,功能全面、生态成熟 | 仅需简单缓存且追求多核利用率时备选 | 阿里云生态内超大规模场景优先 |
选型结论 :Redis 的差异化优势在于"数据结构丰富度 + 持久化 + 完整高可用方案"三位一体,是缓存与内存计算场景的事实标准;Memcached 仅在极简 KV 缓存场景有微弱优势;Tair 属于云厂商定制方案。
二、子知识点分类讲解
本章对 Redis 核心子知识点逐一深度拆解:数据结构与底层实现 → 持久化 → 过期与淘汰 → 缓存三大问题 → 分布式锁 → 主从复制 → 哨兵 → Cluster → 事务与 Lua → 发布订阅与 Stream。
2.1 五大基础数据类型与底层数据结构
2.1.1 子知识点概述
Redis 对外暴露 String、Hash、List、Set、ZSet 五大基础类型,但类型(type)与编码(encoding)是两个维度:同一类型会根据数据规模自动选择不同底层编码,以在"内存占用"与"操作性能"之间取得平衡。这是 Redis 内存效率的核心设计,也是面试深挖的第一重点。
类型与编码映射关系(Redis 7.x):
| 对外类型 | 可选编码 | 编码切换条件 |
|---|---|---|
| String | int / embstr / raw | 整数值可存 long(64位有符号整数);≤44 字节 → embstr(SDS嵌入式);>44 字节 → raw(SDS) |
| Hash | listpack / hashtable | 元素数 >128 或值 >64 字节 → hashtable |
| List | quicklist | 统一使用 quicklist(双向链表串联 listpack) |
| Set | intset / listpack / hashtable | 纯整数且 ≤128 个 → intset;否则视规模升级 |
| ZSet | listpack / skiplist+dict | 元素数 >128 或成员 >64 字节 → 跳表+字典 |
面试重点:单向升级,不会降级 :一旦从小结构(listpack/intset)转为 hashtable/skiplist/raw,数据减少后不会自动变回紧凑编码,只能删 key 重建才会恢复。embstr 只读:只要对 embstr 字符串做修改(append、setrange),redis 会先拷贝成 raw 再修改。
底层核心结构依赖关系:
#mermaid-svg-0006XgHXanQDt6hW{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-0006XgHXanQDt6hW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0006XgHXanQDt6hW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0006XgHXanQDt6hW .error-icon{fill:#552222;}#mermaid-svg-0006XgHXanQDt6hW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0006XgHXanQDt6hW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0006XgHXanQDt6hW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0006XgHXanQDt6hW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0006XgHXanQDt6hW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0006XgHXanQDt6hW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0006XgHXanQDt6hW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0006XgHXanQDt6hW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0006XgHXanQDt6hW .marker.cross{stroke:#333333;}#mermaid-svg-0006XgHXanQDt6hW svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0006XgHXanQDt6hW p{margin:0;}#mermaid-svg-0006XgHXanQDt6hW .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-0006XgHXanQDt6hW .cluster-label text{fill:#333;}#mermaid-svg-0006XgHXanQDt6hW .cluster-label span{color:#333;}#mermaid-svg-0006XgHXanQDt6hW .cluster-label span p{background-color:transparent;}#mermaid-svg-0006XgHXanQDt6hW .label text,#mermaid-svg-0006XgHXanQDt6hW span{fill:#333;color:#333;}#mermaid-svg-0006XgHXanQDt6hW .node rect,#mermaid-svg-0006XgHXanQDt6hW .node circle,#mermaid-svg-0006XgHXanQDt6hW .node ellipse,#mermaid-svg-0006XgHXanQDt6hW .node polygon,#mermaid-svg-0006XgHXanQDt6hW .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-0006XgHXanQDt6hW .rough-node .label text,#mermaid-svg-0006XgHXanQDt6hW .node .label text,#mermaid-svg-0006XgHXanQDt6hW .image-shape .label,#mermaid-svg-0006XgHXanQDt6hW .icon-shape .label{text-anchor:middle;}#mermaid-svg-0006XgHXanQDt6hW .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-0006XgHXanQDt6hW .rough-node .label,#mermaid-svg-0006XgHXanQDt6hW .node .label,#mermaid-svg-0006XgHXanQDt6hW .image-shape .label,#mermaid-svg-0006XgHXanQDt6hW .icon-shape .label{text-align:center;}#mermaid-svg-0006XgHXanQDt6hW .node.clickable{cursor:pointer;}#mermaid-svg-0006XgHXanQDt6hW .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-0006XgHXanQDt6hW .arrowheadPath{fill:#333333;}#mermaid-svg-0006XgHXanQDt6hW .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-0006XgHXanQDt6hW .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-0006XgHXanQDt6hW .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0006XgHXanQDt6hW .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-0006XgHXanQDt6hW .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0006XgHXanQDt6hW .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-0006XgHXanQDt6hW .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-0006XgHXanQDt6hW .cluster text{fill:#333;}#mermaid-svg-0006XgHXanQDt6hW .cluster span{color:#333;}#mermaid-svg-0006XgHXanQDt6hW div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-0006XgHXanQDt6hW .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-0006XgHXanQDt6hW rect.text{fill:none;stroke-width:0;}#mermaid-svg-0006XgHXanQDt6hW .icon-shape,#mermaid-svg-0006XgHXanQDt6hW .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0006XgHXanQDt6hW .icon-shape p,#mermaid-svg-0006XgHXanQDt6hW .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-0006XgHXanQDt6hW .icon-shape .label rect,#mermaid-svg-0006XgHXanQDt6hW .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0006XgHXanQDt6hW .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-0006XgHXanQDt6hW .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-0006XgHXanQDt6hW :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ptr 指向
redisObject
具体编码结构
dict 字典/哈希表(hashtable)
SDS 简单动态字符串(embstr / raw)
skiplist 跳表
listpack 压缩列表
quicklist 快速列表
intset 整数集合
图注 :所有键值对都封装为 redisObject,其中 type 字段标识逻辑类型、encoding 字段标识底层编码,ptr 指向实际数据结构。
2.1.2 完整运行原理
(1)redisObject 统一对象模型
每个键值对都由 redisObject 封装(源码 server.h):
c
typedef struct redisObject {
unsigned type:4; // 逻辑类型: OBJ_STRING / OBJ_HASH / OBJ_LIST / OBJ_SET / OBJ_ZSET
unsigned encoding:4; // 底层编码: OBJ_ENCODING_RAW / EMBSTR / INT / HT / SKIPLIST...
unsigned lru:24; // LRU/LFU 访问时间戳, 供内存淘汰使用
int refcount; // 引用计数, 用于对象共享与释放
void *ptr; // 指向底层数据结构的指针
} robj;
(2)SDS------所有字符串的基石
Redis 没有直接使用 C 字符串,而是自研 SDS,解决 C 字符串三大缺陷:
c
// sds.h (Redis 3.2+ 二进制安全头)
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; // 已使用长度 → O(1) 获取字符串长度
uint8_t alloc; // 总分配容量(不含头和结尾空字符) → 空间预分配依据
unsigned char flags;// 类型标志位, 低3位区分 sdshdr5/8/16/32/64
char buf[]; // 实际数据区, 二进制安全
};
SDS 核心设计考量:
- O(1) 获取长度 :C 字符串需遍历到
\0(O(N)),SDS 直接读len; - 杜绝缓冲区溢出 :拼接前检查
alloc,不足则自动扩容; - 空间预分配 + 惰性释放 :修改后若
len < 1MB则多分配等量空间,减少连续修改的内存重分配次数; - 二进制安全 :以
len判断结尾而非\0,可存图片、序列化对象; - 五种头类型 :按字符串长度选择最小头部,短字符串节省大量内存(
sdshdr5甚至不存 alloc)。
(3)dict------渐进式 rehash 的哈希表
c
typedef struct dict {
dictType *type; // 类型相关的操作函数表
void *privdata;
dictht ht[2]; // 两张哈希表: ht[0] 日常使用, ht[1] rehash 时使用
long rehashidx; // rehash 进度, -1 表示未进行
...
} dict;
typedef struct dictht {
dictEntry **table; // 桶数组, 大小恒为 2 的幂
unsigned long size;
unsigned long sizemask; // = size - 1, 用位运算取模加速定位
unsigned long used; // 已有节点数
} dictht;
typedef struct dictEntry {
void *key;
union { void *val; uint64_t u64; int64_t s64; double d; } v;
struct dictEntry *next; // 链地址法解决哈希冲突
} dictEntry;
渐进式 rehash 执行链路:
- 触发扩容 :负载因子
used/size ≥ 1(无 BGSAVE 时)或used/size > 5(BGSAVE 期间,避免 COW 内存翻倍),见dict.c中dictResize判断; - 分配 ht[1](#分配 ht1 "citation") :大小为第一个 ≥
used*2的 2^n; - 分步迁移 :
rehashidx从 0 开始,每次对 dict 的增删改查操作顺带迁移一个桶 ,同时后台定时任务(databasesCron)每 100ms 迁移 1ms 的桶; - 双表查询 :rehash 期间先查
ht[0]再查ht[1],新增一律写入ht[1],保证ht[0]只减不增; - 完成 :迁移完毕释放
ht[0],将ht[1]设为ht[0],rehashidx置 -1。
设计考量 :把 O(N) 的一次性 rehash 均摊到每次操作中,避免主线程长时间阻塞------这是"渐进式"思想的经典范例。
(4)skiplist------ZSet 的排序引擎
c
typedef struct zskiplistNode {
sds ele; // 成员字符串
double score; // 分值, 排序依据
struct zskiplistNode *backward; // 后退指针, 支持反向遍历
struct zskiplistLevel {
struct zskiplistNode *forward; // 各层前进指针
unsigned long span; // 跨度, 用于 O(logN) 计算排名
} level[]; // 柔性数组, 每节点层数随机
} zskiplistNode;
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level; // 当前最高层数
} zskiplist;
插入流程(zslInsert,t_zset.c):
- 从最高层头节点出发逐层查找插入位置,沿途记录每层的"前驱节点"到
update[]数组与rank[]数组; - 调用
zslRandomLevel()随机生成层数:每层以 1/4 概率晋升,上限 32 层; - 若新层数高于当前最高层,补齐头节点的新层指针;
- 在各层前驱节点后插入新节点,更新
span跨度与backward指针。
为什么用跳表而不用红黑树?
- 范围查询天然高效:ZRANGEBYSCORE 找到起点后沿最底层链表顺序遍历即可,红黑树需中序遍历且涉及回溯;
- 实现简单:插入删除只需局部改指针,无旋转重平衡;
- 并发友好、内存可控:通过调整晋升概率即可平衡时间与空间;
- 作者 antirez 原话:跳表实现不比平衡树复杂,且调试更友好。
(5)listpack / ziplist------小数据量的极致压缩
ziplist 是一段连续内存:zlbytes | zltail | zllen | entry... | zlend。每个 entry 记录 prevlen(前一节点长度,1 或 5 字节)+ encoding + data。其致命缺陷是 连锁更新(cascade update):某节点长度跨越 254 字节边界时,可能引发后续所有节点 prevlen 字段扩容,最坏 O(N²)。
Redis 6.2 起以 listpack 替代 ziplist:每个 entry 改为记录自身长度而非前节点长度,彻底消除连锁更新,同时保留连续内存、缓存友好的优势。7.0 起 Hash/ZSet 的小编码全面切换为 listpack。
(6)quicklist------List 的统一实现
c
typedef struct quicklist {
quicklistNode *head; // 双向链表头
quicklistNode *tail;
unsigned long count; // 所有 listpack 中元素总数
unsigned long len; // quicklistNode 节点数
int fill : 16; // 每个 listpack 的大小限制 (listpack-max-ziplist-size)
unsigned int compress : 16; // 两端不压缩的节点数 (listpack-max-ziplist-entries)
} quicklist;
即"双向链表 × listpack"的混合结构:既保留链表 O(1) 头尾插入,又通过 listpack 连续内存减少碎片、提升缓存命中率。
2.1.3 源码深度剖析(编码自动升级)
以 Hash 为例,t_hash.c 中判断是否从小编码升级:
c
// 向 listpack 编码的 Hash 添加元素前的检查
if (listpackLength(o->ptr) > server.hash_max_listpack_entries || // 元素数超 128
value_len > server.hash_max_listpack_value) { // 单值超 64 字节
// 转换为 hashtable 编码: 逐条搬迁旧元素
o->encoding = OBJ_ENCODING_HT;
o->ptr = dictCreate(&hashDictType, NULL);
...
}
关键设计考量 :小数据量用紧凑结构省内存(一个 10 字段的 Hash 用 listpack 可比 hashtable 省 5~10 倍内存),数据量增大后果断切换为 O(1)/O(logN) 结构保性能------编码只升不降(升级后即使删除元素也不会退回,避免频繁抖动)。
2.1.4 关键细节与易错点
- embstr 与 raw 的分界线是 44 字节 :
redisObject(16B) +sdshdr8(3B) + 1B 结尾空字符 = 20B,jemalloc 以 64B 为内存类对齐,64-20=44; - 编码升级不可逆:Hash 一旦转为 hashtable,删除元素后不会退回 listpack;
- List 已无 ziplist:7.0 起全部是 quicklist,网上"ziplist 实现的 List"说法已过时;
- ZSet 同时维护 dict + skiplist:dict 提供 O(1) 按成员查分值(ZSCORE),跳表提供范围查询,两者共享成员字符串指针,不重复占内存;
- OBJECT ENCODING key 命令可在线查看键的当前编码,是排查内存问题的利器;
- 整数共享池 :0~9999 的整数对象被全局共享(
shared.integers),引用计数复用。
2.1.5 优劣势与能力边界
- 优势:编码自适应设计在内存与性能间取得极佳平衡;底层结构针对性优化(渐进式 rehash、跳表范围查询);
- 固有缺陷:编码升级阈值(如 128/64)需结合业务调优;大 key 场景下任何紧凑编码都会退化为大对象;
- 适用场景:中小规模数据享受紧凑编码红利;超大规模数据需提前评估 hashtable/skiplist 内存开销;
- 不适用:单键存储超大集合(百万级成员的 ZSet)------应拆分或使用专业组件。
2.2 持久化机制:RDB 与 AOF
2.2.1 子知识点概述
Redis 内存数据必须通过持久化落盘,否则重启即数据全失。持久化是 Redis 区别于 Memcached 的核心能力,包含三种形态:
| 形态 | 机制 | 特点 |
|---|---|---|
| RDB | 某时刻全量内存快照(二进制) | 恢复快、文件紧凑,但可能丢失最后一次快照后的数据 |
| AOF | 追加记录每条写命令 | 数据安全性高(最多丢 1 秒),文件大、恢复慢 |
| 混合持久化(4.0+) | AOF 重写时先写 RDB 格式,再追加增量命令 | 兼顾恢复速度与数据安全,生产推荐 |
2.2.2 完整运行原理
(1)RDB 执行链路
触发方式:save(主线程阻塞执行,生产禁用)、bgsave(fork 子进程)、配置 save 900 1 等 savepoint 自动触发、主从全量复制时主节点自动执行。
bgsave 完整流程:
#mermaid-svg-jD3QUQwQUfN5WYSe{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-jD3QUQwQUfN5WYSe .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-jD3QUQwQUfN5WYSe .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-jD3QUQwQUfN5WYSe .error-icon{fill:#552222;}#mermaid-svg-jD3QUQwQUfN5WYSe .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-jD3QUQwQUfN5WYSe .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-jD3QUQwQUfN5WYSe .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-jD3QUQwQUfN5WYSe .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-jD3QUQwQUfN5WYSe .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-jD3QUQwQUfN5WYSe .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-jD3QUQwQUfN5WYSe .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-jD3QUQwQUfN5WYSe .marker{fill:#333333;stroke:#333333;}#mermaid-svg-jD3QUQwQUfN5WYSe .marker.cross{stroke:#333333;}#mermaid-svg-jD3QUQwQUfN5WYSe svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-jD3QUQwQUfN5WYSe p{margin:0;}#mermaid-svg-jD3QUQwQUfN5WYSe .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-jD3QUQwQUfN5WYSe .cluster-label text{fill:#333;}#mermaid-svg-jD3QUQwQUfN5WYSe .cluster-label span{color:#333;}#mermaid-svg-jD3QUQwQUfN5WYSe .cluster-label span p{background-color:transparent;}#mermaid-svg-jD3QUQwQUfN5WYSe .label text,#mermaid-svg-jD3QUQwQUfN5WYSe span{fill:#333;color:#333;}#mermaid-svg-jD3QUQwQUfN5WYSe .node rect,#mermaid-svg-jD3QUQwQUfN5WYSe .node circle,#mermaid-svg-jD3QUQwQUfN5WYSe .node ellipse,#mermaid-svg-jD3QUQwQUfN5WYSe .node polygon,#mermaid-svg-jD3QUQwQUfN5WYSe .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-jD3QUQwQUfN5WYSe .rough-node .label text,#mermaid-svg-jD3QUQwQUfN5WYSe .node .label text,#mermaid-svg-jD3QUQwQUfN5WYSe .image-shape .label,#mermaid-svg-jD3QUQwQUfN5WYSe .icon-shape .label{text-anchor:middle;}#mermaid-svg-jD3QUQwQUfN5WYSe .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-jD3QUQwQUfN5WYSe .rough-node .label,#mermaid-svg-jD3QUQwQUfN5WYSe .node .label,#mermaid-svg-jD3QUQwQUfN5WYSe .image-shape .label,#mermaid-svg-jD3QUQwQUfN5WYSe .icon-shape .label{text-align:center;}#mermaid-svg-jD3QUQwQUfN5WYSe .node.clickable{cursor:pointer;}#mermaid-svg-jD3QUQwQUfN5WYSe .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-jD3QUQwQUfN5WYSe .arrowheadPath{fill:#333333;}#mermaid-svg-jD3QUQwQUfN5WYSe .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-jD3QUQwQUfN5WYSe .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-jD3QUQwQUfN5WYSe .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-jD3QUQwQUfN5WYSe .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-jD3QUQwQUfN5WYSe .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-jD3QUQwQUfN5WYSe .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-jD3QUQwQUfN5WYSe .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-jD3QUQwQUfN5WYSe .cluster text{fill:#333;}#mermaid-svg-jD3QUQwQUfN5WYSe .cluster span{color:#333;}#mermaid-svg-jD3QUQwQUfN5WYSe div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-jD3QUQwQUfN5WYSe .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-jD3QUQwQUfN5WYSe rect.text{fill:none;stroke-width:0;}#mermaid-svg-jD3QUQwQUfN5WYSe .icon-shape,#mermaid-svg-jD3QUQwQUfN5WYSe .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-jD3QUQwQUfN5WYSe .icon-shape p,#mermaid-svg-jD3QUQwQUfN5WYSe .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-jD3QUQwQUfN5WYSe .icon-shape .label rect,#mermaid-svg-jD3QUQwQUfN5WYSe .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-jD3QUQwQUfN5WYSe .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-jD3QUQwQUfN5WYSe .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-jD3QUQwQUfN5WYSe :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
执行 BGSAVE 命令
是否已有子进程在运行?
拒绝执行
主进程 fork 子进程
子进程共享父进程内存页
写临时 RDB 文件
写完后 rename 替换旧 RDB
主进程继续服务客户端
写操作触发 COW?
内核复制被修改的内存页
子进程仍见旧数据
核心机制------写时复制(Copy-On-Write) :fork() 后父子进程共享物理内存页,仅当父进程修改某页时,内核才复制该页。因此 RDB 快照是 fork 瞬间的一致性视图,且额外内存开销与"快照期间被修改的页数量"成正比。
(2)AOF 执行链路
AOF 持久化分三步:命令追加 → 文件写入 → 文件同步(fsync)。
c
// server.c: call() 执行命令后的传播逻辑
void call(client *c, int flags) {
...
dirty += server.dirty - dirty_before; // dirty 记录自上次保存以来的写命令数
...
}
// 事件循环每轮末尾: beforeSleep() → flushAppendOnlyFile()
// 根据 appendfsync 策略决定是否调用 fsync 落盘
三种 fsync 策略(appendfsync):
| 策略 | 行为 | 数据安全 | 性能影响 |
|---|---|---|---|
| always | 每条命令立即 fsync | 最多丢 1 条命令 | 每条命令一次磁盘写,最慢 |
| everysec(推荐) | 后台线程每秒 fsync 一次 | 最多丢 1 秒数据 | 折中方案 |
| no | 交给操作系统决定刷盘时机 | 可能丢数十秒数据 | 最快 |
AOF 重写(rewrite) :AOF 文件随命令累积不断膨胀,重写机制通过"读取当前内存状态、生成最精简命令集"压缩文件(如 100 次 INCR 合并为 1 条 SET)。关键点:
bgrewriteaoffork 子进程,重写期间新的写命令同时写入 AOF 缓冲区和 AOF 重写缓冲区;- 子进程完成重写后,将重写缓冲区的增量命令追加到新文件,原子替换旧文件;
- 重写期间为避免磁盘竞争,主进程对旧 AOF 的 fsync 降频为每 2 秒一次(
aof_rewrite_scheduled)。
(3)Redis 7.0 Multi Part AOF
7.0 将单一 AOF 文件拆分为 base.aof(基线)+ incr.aof(增量)+ manifest(清单文件),重写期间不再需要重写缓冲区,消除了重写结束时的"追增量"阻塞窗口,且支持增量 AOF 的分段管理。
(4)混合持久化(4.0+,aof-use-rdb-preamble yes)
AOF 重写时,新 AOF 文件前半段为 RDB 格式的全量快照,后半段为 AOF 格式的增量命令。恢复时先按 RDB 快速加载,再重放增量命令------恢复速度与数据安全性兼得。
2.2.3 源码深度剖析
rdb.c 中 rdbSaveBackground 的 fork 逻辑:
c
int rdbSaveBackground(char *filename, rdbSaveInfo *rsi) {
pid_t childpid;
if ((childpid = redisFork()) == 0) {
// 子进程: 关闭监听 socket, 遍历所有 db 写 RDB
redisSetProcTitle("redis-rdb-bgsave");
retval = rdbSave(filename, rsi); // 核心: 遍历 server.db 序列化
exitFromChild((retval == C_OK) ? 0 : 1);
} else {
// 父进程: 记录子进程信息, 继续服务
server.stat_fork_time = ...;
server.rdb_child_pid = childpid;
...
}
}
aof.c 中 everysec 策略的 fsync 由 bio 后台线程 执行(bio.c 定义了 BIO_CLOSE_FILE、BIO_AOF_FSYNC、BIO_LAZY_FREE 三类后台任务),避免主线程被磁盘 I/O 阻塞。
关键设计考量 :Redis 把所有耗时操作(RDB 保存、AOF fsync、大 key 异步删除)全部下沉到子进程或 bio 线程,主线程永远只做命令执行------这是 Redis 低延迟的根本保障。
2.2.4 关键细节与易错点
- fork 耗时与内存成正比 :大内存实例(>10GB)fork 可能耗时数百毫秒甚至秒级,引发抖动;可通过
latest_latency/INFO persistence的latest_fork_usec监控; - COW 内存放大:快照期间写流量越大,COW 复制的页越多,峰值内存可能接近 2 倍,需预留内存并避免在业务高峰 BGSAVE;
- AOF 文件损坏恢复 :
redis-check-aof --fix可截断损坏的尾部命令; - AOF 的 fsync 卡死问题 :磁盘负载高时 fsync 超时,Redis 会记录日志并暂停写入(
aof-last-write-status),需监控磁盘; - RDB 不适合做实时备份:savepoint 是"条件触发",两次快照间宕机数据全丢;
- 主从复制与 RDB 耦合 :全量同步依赖 RDB,因此主节点上
disable_bgsave需慎重。
2.2.5 优劣势与能力边界
- RDB 优势 :恢复速度极快(直接加载二进制)、文件紧凑适合异地备份;缺陷:数据安全性差、fork 抖动;
- AOF 优势 :最多丢 1 秒数据、文件可读可修复;缺陷:文件大、恢复慢、写放大;
- 混合持久化:生产环境事实标准,但要求 Redis ≥ 4.0;
- 边界:持久化不能替代备份策略------仍需定期把 RDB 归档到对象存储。
2.3 过期策略与内存淘汰机制
2.3.1 子知识点概述
Redis 是内存数据库,内存是稀缺资源。本子知识点解决两个问题:
- 过期键如何被删除------过期策略(针对设置了 TTL 的键);
- 内存写满时怎么办------内存淘汰策略(maxmemory 达到上限后的兜底)。
两者常被混淆:过期策略处理"到期键",淘汰策略处理"内存不够",淘汰策略的候选范围可以是全部键或仅过期键。
2.3.2 完整运行原理
(1)过期键的存储
所有过期时间集中存储在 db->expires 字典中(键为指针,值为毫秒级时间戳),与主字典 db->dict 分离,避免无过期键的额外开销。
(2)三种过期删除策略
| 策略 | 机制 | 优劣 |
|---|---|---|
| 定时删除 | 设置 TTL 时创建定时器,到期立即删 | 内存友好,但 CPU 开销大,Redis 未采用 |
| 惰性删除 | 访问键时才检查是否过期,过期则删 | CPU 友好,但过期键可能长期占内存 |
| 定期删除 | 周期性随机抽样检查并删除过期键 | 折中方案 |
Redis 采用"惰性删除 + 定期删除"组合:
- 惰性删除 :所有读写命令执行前经过
expireIfNeeded()检查; - 定期删除 :由时间事件
databasesCron()→activeExpireCycle()驱动,每 100ms 执行一次:
c
// expire.c: activeExpireCycle 核心逻辑(简化)
// 每轮对每个 db 抽样检查
unsigned long expired;
do {
// 1. 从 expires 字典随机抽 ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP(默认20) 个键
for (i = 0; i < ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP; i++) {
entry = dictGetRandomKey(db->expires);
if (activeExpireCycleTryExpire(db, entry, now)) expired++; // 到期则删除
}
// 2. 若过期比例 > 25%, 说明过期键密集, 继续抽样
// 3. 但总耗时不得超过时间上限(默认 25ms), 超时则下轮再战
} while (percentage > 25 && elapsed < timelimit);
关键分支逻辑:
- 抽样过期率 >25% → 继续循环,保证过期键及时清理;
- 执行超时 → 立即退出,绝不阻塞主线程;
- 从库不主动删除过期键:等待主节点同步 DEL 命令,保证主从数据一致(这也是读写分离场景下从库可能读到过期数据的原因)。
(3)八种内存淘汰策略(maxmemory-policy)
| 策略 | 候选范围 | 算法 | 适用场景 |
|---|---|---|---|
| noeviction | --- | 不淘汰,写操作直接报错 | 不允许丢数据的缓存 |
| allkeys-lru | 全部键 | 近似 LRU(最近最少使用) | 通用缓存首选 |
| volatile-lru | 设了 TTL 的键 | 近似 LRU(最近最少使用) | 部分键需常驻 |
| allkeys-lfu | 全部键 | 近似 LFU(4.0+)(最不经常使用) | 热点数据缓存 |
| volatile-lfu | 设了 TTL 的键 | 近似 LFU (最不经常使用) | 热点+常驻混合 |
| allkeys-random | 全部键 | 随机 | 访问分布均匀 |
| volatile-random | 设了 TTL 的键 | 随机 | 同上 |
| volatile-ttl | 设了 TTL 的键 | TTL 最小者优先 | 明确希望快过期的先走 |
(4)近似 LRU 的实现原理
Redis 没有实现严格 LRU(需维护全局双向链表,内存开销大),而是采样近似:
c
// evict.c: evictionPoolPopulate
// 随机采样 maxmemory-samples(默认5) 个键, 按空闲时间排序放入淘汰池
// 淘汰时选择池中"最久未访问"的键
redisObject.lru 字段(24 位)记录访问时间,采样 5 个后淘汰最旧者。Redis 3.0 起引入淘汰池(eviction pool):维护 16 个候选键,新采样键与池中键比较替换,使近似效果非常接近严格 LRU。
LFU(4.0+) :复用 lru 字段,拆分为 16 位"上次访问时间"+ 8 位"访问频率计数器"(对数计数,带时间衰减),解决 LRU 无法识别"低频但最近被访问"键的问题------缓存污染场景下 LFU 显著优于 LRU(如一次性全表扫描把热点挤出去)。
2.3.3 关键细节与易错点
- 过期时间精度 :存储为毫秒时间戳,但
EXPIRE命令参数是秒;主从复制时过期时间以绝对时间戳传播,不受主从时钟偏差影响; - 从库只读特性:从库不执行惰性/定期删除,读从库可能读到"逻辑已过期"的数据;
- 淘汰发生在写路径 :只有写命令触发
performEvictions(),纯读不会淘汰; - LRU 采样数可调 :
maxmemory-samples 10可提升精度但增加 CPU 开销; - noeviction 的报错是 OOM command not allowed------业务侧必须处理该异常;
- maxmemory 不包含副本缓冲:实际内存占用 = 数据 + 复制缓冲 + Lua 缓存等,需预留 20%~30%。
2.3.4 优劣势与能力边界
- 优势:惰性+定期的组合在 CPU 与内存间取得工程最优;近似 LRU/LFU 以极小开销逼近理论效果;
- 缺陷:定期删除是"抽样"而非全量,极端情况下过期键清理滞后;淘汰策略作用于单实例,集群场景需整体容量规划;
- 适用 :所有缓存场景必配
maxmemory+ 合理淘汰策略; - 不适用:把 Redis 当持久数据库且不允许任何数据丢失------应选 noeviction 并监控内存水位。
2.4 缓存穿透、缓存击穿、缓存雪崩
2.4.1 子知识点概述
这是缓存层三大经典故障模型,本质都是"缓存与数据库的一致性/可用性失衡导致流量打穿数据库"。三者的区别是面试必考的辨析点:
| 问题 | 定义 | 核心特征 |
|---|---|---|
| 缓存穿透 | 查询根本不存在的数据,缓存无、数据库也无 | 请求绕过缓存直达 DB,缓存形同虚设 |
| 缓存击穿 | 单个热点键过期瞬间,海量并发同时打到 DB | 问题集中在一个键,瞬时洪峰 |
| 缓存雪崩 | 大量键同时过期或缓存服务宕机,请求集中打到 DB | 大面积、持续性压力,可能引发级联故障 |
2.4.2 完整运行原理与解决方案
(1)缓存穿透
#mermaid-svg-BoK2NiiSo2t3iVy6{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-BoK2NiiSo2t3iVy6 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-BoK2NiiSo2t3iVy6 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-BoK2NiiSo2t3iVy6 .error-icon{fill:#552222;}#mermaid-svg-BoK2NiiSo2t3iVy6 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-BoK2NiiSo2t3iVy6 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-BoK2NiiSo2t3iVy6 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-BoK2NiiSo2t3iVy6 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-BoK2NiiSo2t3iVy6 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-BoK2NiiSo2t3iVy6 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-BoK2NiiSo2t3iVy6 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-BoK2NiiSo2t3iVy6 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-BoK2NiiSo2t3iVy6 .marker.cross{stroke:#333333;}#mermaid-svg-BoK2NiiSo2t3iVy6 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-BoK2NiiSo2t3iVy6 p{margin:0;}#mermaid-svg-BoK2NiiSo2t3iVy6 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-BoK2NiiSo2t3iVy6 .cluster-label text{fill:#333;}#mermaid-svg-BoK2NiiSo2t3iVy6 .cluster-label span{color:#333;}#mermaid-svg-BoK2NiiSo2t3iVy6 .cluster-label span p{background-color:transparent;}#mermaid-svg-BoK2NiiSo2t3iVy6 .label text,#mermaid-svg-BoK2NiiSo2t3iVy6 span{fill:#333;color:#333;}#mermaid-svg-BoK2NiiSo2t3iVy6 .node rect,#mermaid-svg-BoK2NiiSo2t3iVy6 .node circle,#mermaid-svg-BoK2NiiSo2t3iVy6 .node ellipse,#mermaid-svg-BoK2NiiSo2t3iVy6 .node polygon,#mermaid-svg-BoK2NiiSo2t3iVy6 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-BoK2NiiSo2t3iVy6 .rough-node .label text,#mermaid-svg-BoK2NiiSo2t3iVy6 .node .label text,#mermaid-svg-BoK2NiiSo2t3iVy6 .image-shape .label,#mermaid-svg-BoK2NiiSo2t3iVy6 .icon-shape .label{text-anchor:middle;}#mermaid-svg-BoK2NiiSo2t3iVy6 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-BoK2NiiSo2t3iVy6 .rough-node .label,#mermaid-svg-BoK2NiiSo2t3iVy6 .node .label,#mermaid-svg-BoK2NiiSo2t3iVy6 .image-shape .label,#mermaid-svg-BoK2NiiSo2t3iVy6 .icon-shape .label{text-align:center;}#mermaid-svg-BoK2NiiSo2t3iVy6 .node.clickable{cursor:pointer;}#mermaid-svg-BoK2NiiSo2t3iVy6 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-BoK2NiiSo2t3iVy6 .arrowheadPath{fill:#333333;}#mermaid-svg-BoK2NiiSo2t3iVy6 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-BoK2NiiSo2t3iVy6 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-BoK2NiiSo2t3iVy6 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BoK2NiiSo2t3iVy6 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-BoK2NiiSo2t3iVy6 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BoK2NiiSo2t3iVy6 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-BoK2NiiSo2t3iVy6 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-BoK2NiiSo2t3iVy6 .cluster text{fill:#333;}#mermaid-svg-BoK2NiiSo2t3iVy6 .cluster span{color:#333;}#mermaid-svg-BoK2NiiSo2t3iVy6 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-BoK2NiiSo2t3iVy6 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-BoK2NiiSo2t3iVy6 rect.text{fill:none;stroke-width:0;}#mermaid-svg-BoK2NiiSo2t3iVy6 .icon-shape,#mermaid-svg-BoK2NiiSo2t3iVy6 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BoK2NiiSo2t3iVy6 .icon-shape p,#mermaid-svg-BoK2NiiSo2t3iVy6 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-BoK2NiiSo2t3iVy6 .icon-shape .label rect,#mermaid-svg-BoK2NiiSo2t3iVy6 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BoK2NiiSo2t3iVy6 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-BoK2NiiSo2t3iVy6 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-BoK2NiiSo2t3iVy6 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 未命中
不存在
每次请求都重复此路径
请求 id=-1 的商品
查缓存
查数据库
返回空
恶意攻击/脏数据
攻击者利用"不存在的数据"绕过缓存,因为缓存层对"不存在"没有记忆。解决方案:
- 缓存空对象 :数据库查不到时,向 Redis 写入空值(如
SET product:-1 "" EX 60),短 TTL 防止长期占用;实现简单,但无法防御海量随机不存在的 key(内存被撑爆); - 布隆过滤器前置拦截:将所有合法 ID 预先载入布隆过滤器,请求先过 BF,判定"一定不存在"的直接拒绝。误判率可控(通常 <1%),内存开销极小(1 亿 key 约 100MB);
- 参数合法性校验:id ≤ 0 等非法参数在网关层直接拦截,属于第一道防线。
(2)缓存击穿
#mermaid-svg-Z1oYVa62PwzROb5P{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Z1oYVa62PwzROb5P .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Z1oYVa62PwzROb5P .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Z1oYVa62PwzROb5P .error-icon{fill:#552222;}#mermaid-svg-Z1oYVa62PwzROb5P .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Z1oYVa62PwzROb5P .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Z1oYVa62PwzROb5P .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Z1oYVa62PwzROb5P .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Z1oYVa62PwzROb5P .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Z1oYVa62PwzROb5P .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Z1oYVa62PwzROb5P .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Z1oYVa62PwzROb5P .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Z1oYVa62PwzROb5P .marker.cross{stroke:#333333;}#mermaid-svg-Z1oYVa62PwzROb5P svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Z1oYVa62PwzROb5P p{margin:0;}#mermaid-svg-Z1oYVa62PwzROb5P .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Z1oYVa62PwzROb5P .cluster-label text{fill:#333;}#mermaid-svg-Z1oYVa62PwzROb5P .cluster-label span{color:#333;}#mermaid-svg-Z1oYVa62PwzROb5P .cluster-label span p{background-color:transparent;}#mermaid-svg-Z1oYVa62PwzROb5P .label text,#mermaid-svg-Z1oYVa62PwzROb5P span{fill:#333;color:#333;}#mermaid-svg-Z1oYVa62PwzROb5P .node rect,#mermaid-svg-Z1oYVa62PwzROb5P .node circle,#mermaid-svg-Z1oYVa62PwzROb5P .node ellipse,#mermaid-svg-Z1oYVa62PwzROb5P .node polygon,#mermaid-svg-Z1oYVa62PwzROb5P .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Z1oYVa62PwzROb5P .rough-node .label text,#mermaid-svg-Z1oYVa62PwzROb5P .node .label text,#mermaid-svg-Z1oYVa62PwzROb5P .image-shape .label,#mermaid-svg-Z1oYVa62PwzROb5P .icon-shape .label{text-anchor:middle;}#mermaid-svg-Z1oYVa62PwzROb5P .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Z1oYVa62PwzROb5P .rough-node .label,#mermaid-svg-Z1oYVa62PwzROb5P .node .label,#mermaid-svg-Z1oYVa62PwzROb5P .image-shape .label,#mermaid-svg-Z1oYVa62PwzROb5P .icon-shape .label{text-align:center;}#mermaid-svg-Z1oYVa62PwzROb5P .node.clickable{cursor:pointer;}#mermaid-svg-Z1oYVa62PwzROb5P .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Z1oYVa62PwzROb5P .arrowheadPath{fill:#333333;}#mermaid-svg-Z1oYVa62PwzROb5P .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Z1oYVa62PwzROb5P .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Z1oYVa62PwzROb5P .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Z1oYVa62PwzROb5P .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Z1oYVa62PwzROb5P .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Z1oYVa62PwzROb5P .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Z1oYVa62PwzROb5P .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Z1oYVa62PwzROb5P .cluster text{fill:#333;}#mermaid-svg-Z1oYVa62PwzROb5P .cluster span{color:#333;}#mermaid-svg-Z1oYVa62PwzROb5P div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Z1oYVa62PwzROb5P .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Z1oYVa62PwzROb5P rect.text{fill:none;stroke-width:0;}#mermaid-svg-Z1oYVa62PwzROb5P .icon-shape,#mermaid-svg-Z1oYVa62PwzROb5P .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Z1oYVa62PwzROb5P .icon-shape p,#mermaid-svg-Z1oYVa62PwzROb5P .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Z1oYVa62PwzROb5P .icon-shape .label rect,#mermaid-svg-Z1oYVa62PwzROb5P .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Z1oYVa62PwzROb5P .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Z1oYVa62PwzROb5P .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Z1oYVa62PwzROb5P :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 热点 key 过期
并发请求 1..N 同时到达
缓存未命中
请求 1 获得互斥锁
请求 2..N 等待/重试
查 DB 并重建缓存
释放锁
重新读缓存, 命中
解决方案:
- 互斥锁重建(推荐) :缓存未命中时,用
SET lock_key unique_val NX EX 10抢锁,只有拿到锁的线程去查 DB 重建缓存,其余线程短暂等待后重试。注意锁要设 TTL 防止死锁,释放锁时校验 unique_val 防误删; - 逻辑过期(不设 TTL) :热点键永不过期,值中内嵌逻辑过期时间;读到"逻辑过期"的旧值时,开异步线程重建缓存,当前请求先返回旧值------牺牲短暂一致性换取绝对可用性,适合对一致性要求不高的热点数据;
- 热点预加载与永不过期:对可预知的热点(如秒杀商品),上线前预热并配合后台定时刷新。
(3)缓存雪崩
雪崩有两种成因,解法不同:
| 成因 | 解决方案 |
|---|---|
| 大量键同时过期 | 过期时间加随机抖动 :TTL = base + random(0, 300),打散过期时刻;热点数据永不过期 + 异步刷新 |
| 缓存服务宕机 | 高可用架构 :Sentinel/Cluster 保证缓存层不整体失效;多级缓存 :本地缓存(Caffeine)+ Redis 兜底;熔断限流降级:Sentinel/Hystrix 限制打到 DB 的流量阈值,超出直接返回降级数据 |
此外通用兜底手段:数据库层限流 (连接池/令牌桶)、缓存预热 (服务启动时批量加载)、构建缓存更新队列串行化重建,避免 DB 被瞬时打垮。
2.4.3 关键细节与易错点
- 布隆过滤器不支持删除(标准版),删除场景需用 Counting Bloom Filter 或定期重建;
- 互斥锁方案中,等待线程若无限重试会占用线程池,应设最大重试次数与退避;
- 逻辑过期方案的"异步重建"必须保证只有一个线程在重建(仍需分布式锁或 CAS 标记),否则重复重建浪费资源;
- 雪崩的"随机抖动"不是越大越好,需结合数据时效性要求权衡;
- 三大问题常并发出现:雪崩期间大量键失效 → 重建洪峰 → 演变为大面积击穿,因此架构上要组合设防。
2.4.4 优劣势与能力边界
- 三大方案本质是"空间换时间、一致性换可用性、复杂度换稳定性"的权衡;
- 布隆过滤器适合读多写少、可容忍极低误判的场景;
- 互斥锁适合一致性要求高的场景,但会引入锁竞争与短暂请求等待;
- 逻辑过期适合热点明确、可容忍秒级旧数据的场景;
- 边界:所有方案都不能替代数据库自身的容量与限流设计,DB 永远是最后一道防线。
2.5 Redis 分布式锁
2.5.1 子知识点概述
分布式锁是 Redis 最高频的实战考点。其本质是:利用 Redis 单线程命令执行的原子性,用一个键的存在性/值来表达"锁的持有状态" 。核心要求是互斥、防死锁、可重入、高可用、可续期。
2.5.2 完整运行原理
(1)基础加锁原语
redis
SET lock_key unique_request_id NX EX 30
NX:键不存在才设置成功 → 保证互斥;EX 30:自动过期 → 防止持锁进程崩溃导致死锁;unique_request_id:标识锁的持有者 → 解锁时校验,防止误删他人锁。
(2)解锁必须用 Lua 保证原子性
"判断值是否为自己 + 删除"两步操作若非原子,会出现误删:
lua
-- unlock.lua
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
(3)锁续期(看门狗机制)
固定过期时间面临两难:设短了业务没执行完锁就过期(导致锁失效、他人进入临界区);设长了持锁者崩溃后锁迟迟不释放。**看门狗(Watchdog)**方案:初始过期时间设为 30 秒,后台线程每隔 10 秒(1/3 周期)检查持锁线程是否存活,存活则续期回 30 秒;线程结束或进程崩溃则停止续期,锁自然过期。Redisson 的 lock()(不指定 leaseTime)默认启用看门狗。
(4)可重入
Redisson 用 Hash 结构记录重入次数:
redis
HSET lock_key <clientId:threadId> 1 -- 首次加锁
HINCRBY lock_key <clientId:threadId> 1 -- 重入时计数+1
解锁时计数减 1,归零才真正删除键。
(5)主从切换下的锁丢失与 Redlock
主节点加锁成功后、尚未同步到从节点时主节点宕机,从节点晋升为新主------锁丢失 ,另一客户端可在新主上再次加锁,互斥被破坏。针对此问题,antirez 提出 Redlock 算法:
- 向 N 个(建议 5 个)相互独立的 Redis 实例,用相同 key/value 依次尝试加锁;
- 只有在**大多数(N/2+1)**实例上加锁成功、且总耗时小于锁有效期,才算加锁成功;
- 锁的实际有效时间 = 设定有效期 − 加锁耗时;
- 失败则向所有实例发送解锁请求(即使某些实例没加上锁也要发,防止残留)。
Redlock 存在争议(Martin Kleppmann 指出其依赖时钟假设、GC 暂停等问题),工程上更常见的取舍是:对强一致性要求极高的场景用 ZooKeeper/etcd 的 CP 型锁;对性能敏感、可容忍极端情况下短暂双写的场景用单主/哨兵 Redis 锁 + 业务幂等兜底。
2.5.3 源码/实现剖析(Redisson 加锁核心逻辑伪代码)
lua
-- Redisson tryLock 的 Lua 脚本(简化还原)
if (redis.call('exists', KEYS[1]) == 0) then
redis.call('hincrby', KEYS[1], ARGV[2], 1); -- 无锁: 记录持有者, 计数1
redis.call('pexpire', KEYS[1], ARGV[1]); -- 设置过期时间
return nil; -- nil 表示加锁成功
end;
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
redis.call('hincrby', KEYS[1], ARGV[2], 1); -- 已持有: 重入计数+1
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
return redis.call('pttl', KEYS[1]); -- 锁被他人持有: 返回剩余TTL, 供订阅等待
设计考量:加锁失败时返回剩余 TTL,客户端据此订阅该键的过期事件(Pub/Sub),锁释放时被唤醒重试------避免盲目轮询带来的性能浪费。
2.5.4 关键细节与易错点
- 绝不用
SETNX+EXPIRE两条命令 :非原子,中间崩溃即死锁,必须用SET key val NX EX; - 解锁必须校验持有者:否则慢请求超时后解锁会删掉新持有者的锁;
- 锁粒度要细 :
lock:order:{orderId}优于lock:order,避免串行化整个业务; - 看门狗只解决"续期",不解决"主从切换丢锁":两者是独立问题;
- 锁内操作要快:临界区耗时越长,锁竞争与超时风险越大;
- 集群模式下锁键与订阅:Redisson 依赖 Pub/Sub 通知,集群中订阅会自动路由到锁所在节点。
2.5.5 优劣势与能力边界
- 优势:性能远高于数据库锁,实现轻量,Redisson 封装成熟(可重入、看门狗、公平锁、联锁);
- 缺陷:AP 属性决定其在主从切换瞬间无法保证绝对互斥;Redlock 复杂且依赖时钟;
- 适用:缓存重建互斥、定时任务防重、库存扣减串行化等绝大多数互联网场景;
- 不适用:金融级强一致互斥------应选 CP 型协调服务或数据库唯一约束兜底。
2.6 主从复制
2.6.1 子知识点概述
主从复制是 Redis 高可用的基石:从节点复制主节点数据,实现读写分离、数据冗余、故障恢复的基础 。复制分为两个阶段:全量复制(首次同步/断线过久)与增量复制(断线重连后的差异补齐) ,核心机制是 PSYNC2 协议 + 复制积压缓冲区(repl_backlog)+ 复制 ID(replid)。
2.6.2 完整运行原理
(1)全量复制流程(首次同步)
主节点 从节点 主节点 从节点 #mermaid-svg-bjguGbxxPbKR2xOz{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-bjguGbxxPbKR2xOz .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-bjguGbxxPbKR2xOz .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-bjguGbxxPbKR2xOz .error-icon{fill:#552222;}#mermaid-svg-bjguGbxxPbKR2xOz .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-bjguGbxxPbKR2xOz .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-bjguGbxxPbKR2xOz .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-bjguGbxxPbKR2xOz .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-bjguGbxxPbKR2xOz .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-bjguGbxxPbKR2xOz .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-bjguGbxxPbKR2xOz .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-bjguGbxxPbKR2xOz .marker{fill:#333333;stroke:#333333;}#mermaid-svg-bjguGbxxPbKR2xOz .marker.cross{stroke:#333333;}#mermaid-svg-bjguGbxxPbKR2xOz svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-bjguGbxxPbKR2xOz p{margin:0;}#mermaid-svg-bjguGbxxPbKR2xOz .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-bjguGbxxPbKR2xOz text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-bjguGbxxPbKR2xOz .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-bjguGbxxPbKR2xOz .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-bjguGbxxPbKR2xOz .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-bjguGbxxPbKR2xOz .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-bjguGbxxPbKR2xOz #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-bjguGbxxPbKR2xOz .sequenceNumber{fill:white;}#mermaid-svg-bjguGbxxPbKR2xOz #sequencenumber{fill:#333;}#mermaid-svg-bjguGbxxPbKR2xOz #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-bjguGbxxPbKR2xOz .messageText{fill:#333;stroke:none;}#mermaid-svg-bjguGbxxPbKR2xOz .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-bjguGbxxPbKR2xOz .labelText,#mermaid-svg-bjguGbxxPbKR2xOz .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-bjguGbxxPbKR2xOz .loopText,#mermaid-svg-bjguGbxxPbKR2xOz .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-bjguGbxxPbKR2xOz .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-bjguGbxxPbKR2xOz .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-bjguGbxxPbKR2xOz .noteText,#mermaid-svg-bjguGbxxPbKR2xOz .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-bjguGbxxPbKR2xOz .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-bjguGbxxPbKR2xOz .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-bjguGbxxPbKR2xOz .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-bjguGbxxPbKR2xOz .actorPopupMenu{position:absolute;}#mermaid-svg-bjguGbxxPbKR2xOz .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-bjguGbxxPbKR2xOz .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-bjguGbxxPbKR2xOz .actor-man circle,#mermaid-svg-bjguGbxxPbKR2xOz line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-bjguGbxxPbKR2xOz :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} loop 命令传播(持续) PSYNC ? -1 (首次, 无 replid) +FULLRESYNC replid offset BGSAVE 生成 RDB (fork) 期间写命令存入"复制缓冲区" 发送 RDB 文件 清空旧数据, 加载 RDB 发送复制缓冲区中的增量命令 执行增量命令, 追平主节点 异步发送写命令
(2)增量复制(断线重连)
从节点重连后发送 PSYNC <replid> <offset>:
- 若主节点发现该 offset 仍在复制积压缓冲区 (环形缓冲,默认 1MB)内 → 部分重同步(+CONTINUE),只补发缺失的命令,代价极小;
- 若 offset 已滑出缓冲区,或 replid 不匹配 → 退化为全量复制。
关键细节 :主节点的 replid 在每次从节点晋升为主时会变化;Redis 4.0 引入 replid2 记录"上一任主节点的 replid",使得故障切换后新主仍能与旧从节点部分重同步,避免切换引发的全量风暴。
(3)命令传播与心跳
- 主节点每次执行写命令后,向所有从节点传播该命令(异步);
- 主从互发心跳:从节点默认每秒发
REPLCONF ACK <offset>,主节点据此判断从节点状态与延迟(master_link_status、slave_repl_offset); REPLCONF ACK还承担检测复制延迟 与保证 AOF 持久化级别 (min-replicas-to-write)的职责。
(4)无盘复制
repl-diskless-sync yes:主节点不落地 RDB 文件,直接通过 socket 把 RDB 流式发给从节点,适合磁盘慢但网络快的场景。
2.6.3 源码要点
replication.c 中 masterTryPartialResynchronization 判断部分重同步:
c
// 校验 replid 是否匹配(当前或前任), offset 是否在 backlog 范围内
if (replid 匹配 && offset >= master_repl_offset - backlog_size) {
// 返回 +CONTINUE, 从 backlog 中补发 [offset, master_repl_offset] 区间命令
} else {
// 走全量: startBgsaveForReplication
}
2.6.4 关键细节与易错点
- 复制是异步的:主从存在毫秒级延迟,读写分离时从库可能读到旧数据,强一致读需读主;
- 全量复制代价巨大 :fork + RDB 传输 + 从库清空重载,大实例全量可能持续分钟级,repl_backlog 要足够大(建议按"断线时长 × 写入速率"估算)以避免频繁全量;
- 从节点默认只读 :
replica-read-only yes,且从节点的写不会同步给主; - 复制缓冲区的内存 :每个从节点有独立的客户端输出缓冲,从节点过多或网络慢会导致主节点内存暴涨(
client-output-buffer-limit replica需合理配置); - 主从复制不保证事务命令的顺序完全一致(Lua/MULTI 作为整体传播,但多从之间到达顺序可能不同------单从内是一致的)。
2.6.5 优劣势与能力边界
- 优势:架构简单、读写分离扩展读能力、为哨兵/集群提供基础;
- 缺陷:异步复制有数据丢失窗口;全量复制开销大;写能力无法扩展(单主);
- 适用:读多写少业务的读扩展、数据冗余备份;
- 不适用:需要强一致读、或写压力需要水平扩展的场景(应上 Cluster)。
2.7 Sentinel 哨兵
2.7.1 子知识点概述
主从架构的痛点是主节点故障后需要人工切换 。Sentinel 是一个独立运行的进程集群,负责:监控 主从健康、自动故障转移 、配置中心 (向客户端通告当前主节点地址)、通知。Sentinel 本身以集群方式部署(至少 3 个,奇数),避免单点误判。
2.7.2 完整运行原理
(1)监控机制
- 每个 Sentinel 每 1 秒向主/从节点及其他 Sentinel 发送
PING; - 若某实例在
down-after-milliseconds内未有效回复 → 该 Sentinel 判定其主观下线(SDOWN); - 对主节点的 SDOWN,Sentinel 会询问其他 Sentinel;当超过
quorum个 Sentinel 都认为主节点下线 → 客观下线(ODOWN)。从节点和哨兵自身只会有主观下线,不会触发故障转移。
(2)故障转移流程
#mermaid-svg-wlHuGmDMP8GeI8SO{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-wlHuGmDMP8GeI8SO .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wlHuGmDMP8GeI8SO .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wlHuGmDMP8GeI8SO .error-icon{fill:#552222;}#mermaid-svg-wlHuGmDMP8GeI8SO .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wlHuGmDMP8GeI8SO .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wlHuGmDMP8GeI8SO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wlHuGmDMP8GeI8SO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wlHuGmDMP8GeI8SO .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wlHuGmDMP8GeI8SO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wlHuGmDMP8GeI8SO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wlHuGmDMP8GeI8SO .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wlHuGmDMP8GeI8SO .marker.cross{stroke:#333333;}#mermaid-svg-wlHuGmDMP8GeI8SO svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wlHuGmDMP8GeI8SO p{margin:0;}#mermaid-svg-wlHuGmDMP8GeI8SO .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-wlHuGmDMP8GeI8SO .cluster-label text{fill:#333;}#mermaid-svg-wlHuGmDMP8GeI8SO .cluster-label span{color:#333;}#mermaid-svg-wlHuGmDMP8GeI8SO .cluster-label span p{background-color:transparent;}#mermaid-svg-wlHuGmDMP8GeI8SO .label text,#mermaid-svg-wlHuGmDMP8GeI8SO span{fill:#333;color:#333;}#mermaid-svg-wlHuGmDMP8GeI8SO .node rect,#mermaid-svg-wlHuGmDMP8GeI8SO .node circle,#mermaid-svg-wlHuGmDMP8GeI8SO .node ellipse,#mermaid-svg-wlHuGmDMP8GeI8SO .node polygon,#mermaid-svg-wlHuGmDMP8GeI8SO .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-wlHuGmDMP8GeI8SO .rough-node .label text,#mermaid-svg-wlHuGmDMP8GeI8SO .node .label text,#mermaid-svg-wlHuGmDMP8GeI8SO .image-shape .label,#mermaid-svg-wlHuGmDMP8GeI8SO .icon-shape .label{text-anchor:middle;}#mermaid-svg-wlHuGmDMP8GeI8SO .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-wlHuGmDMP8GeI8SO .rough-node .label,#mermaid-svg-wlHuGmDMP8GeI8SO .node .label,#mermaid-svg-wlHuGmDMP8GeI8SO .image-shape .label,#mermaid-svg-wlHuGmDMP8GeI8SO .icon-shape .label{text-align:center;}#mermaid-svg-wlHuGmDMP8GeI8SO .node.clickable{cursor:pointer;}#mermaid-svg-wlHuGmDMP8GeI8SO .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-wlHuGmDMP8GeI8SO .arrowheadPath{fill:#333333;}#mermaid-svg-wlHuGmDMP8GeI8SO .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-wlHuGmDMP8GeI8SO .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-wlHuGmDMP8GeI8SO .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wlHuGmDMP8GeI8SO .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-wlHuGmDMP8GeI8SO .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wlHuGmDMP8GeI8SO .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-wlHuGmDMP8GeI8SO .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-wlHuGmDMP8GeI8SO .cluster text{fill:#333;}#mermaid-svg-wlHuGmDMP8GeI8SO .cluster span{color:#333;}#mermaid-svg-wlHuGmDMP8GeI8SO div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-wlHuGmDMP8GeI8SO .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-wlHuGmDMP8GeI8SO rect.text{fill:none;stroke-width:0;}#mermaid-svg-wlHuGmDMP8GeI8SO .icon-shape,#mermaid-svg-wlHuGmDMP8GeI8SO .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wlHuGmDMP8GeI8SO .icon-shape p,#mermaid-svg-wlHuGmDMP8GeI8SO .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-wlHuGmDMP8GeI8SO .icon-shape .label rect,#mermaid-svg-wlHuGmDMP8GeI8SO .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wlHuGmDMP8GeI8SO .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-wlHuGmDMP8GeI8SO .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-wlHuGmDMP8GeI8SO :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 主节点客观下线 ODOWN
Sentinel 间选举 Leader
Leader 执行故障转移
从从节点中选出新主
向新主发送 SLAVEOF NO ONE
让其他从节点复制新主
更新配置, 通知客户端
Leader 选举 :通过 Raft 算法,SDOWN 发生后各 Sentinel 发起投票,获得半数以上且 ≥ quorum 票者成为 Leader 执行转移。
新主选择标准(按优先级依次判断):
- 排除不健康的从节点(下线、断连过久);
slave-priority(replica-priority)数值最小者优先(0 表示永不参选);- 复制偏移量最大者(数据最新)优先;
- runid 最小者兜底。
(3)客户端交互
客户端连接 Sentinel 获取主节点地址,订阅 +switch-master 频道;主从切换后 Sentinel 推送新地址,客户端自动重连------客户端必须实现订阅与重连逻辑(Jedis/Lettuce 的 Sentinel 模式已内置)。
2.7.3 关键细节与易错点
- quorum 与多数派:quorum 只决定 ODOWN,实际故障转移还需 Sentinel 总数过半,因此 3 节点集群容忍 1 个哨兵故障,2 个则无法转移;
- 脑裂风险 :网络分区时旧主可能与哨兵失联而被降级,分区恢复后出现"双主",旧主以从节点身份重新加入会清空数据------可配
min-replicas-to-write让旧主在无从节点时拒绝写入,减小脑裂丢数据窗口; - Sentinel 自身无持久化需求 :其状态通过
INFO与 gossip 实时获取,配置文件仅存初始拓扑; - 故障转移期间写入会丢失:异步复制 + 切换窗口,业务需容忍秒级数据丢失或做幂等。
2.7.4 优劣势与能力边界
- 优势:自动故障转移,运维成本低,客户端接入成熟;
- 缺陷 :写能力与存储容量仍受限于单主,无法水平扩展;切换期间有短暂不可用与数据丢失窗口;
- 适用:数据量单实例可容纳、需要高可用的中小规模场景;
- 不适用:数据量超过单机内存、或写 QPS 超过单机能力的场景------应选 Cluster。
2.8 Redis Cluster 集群
2.8.1 子知识点概述
Redis Cluster 是 Redis 官方的分布式解决方案 ,解决哨兵无法突破的两大瓶颈:数据分片(突破单机内存)与写能力扩展 。核心机制:16384 个哈希槽(hash slot)+ Gossip 协议 + 客户端重定向(MOVED/ASK)+ 集群内自动故障转移。
2.8.2 完整运行原理
(1)数据分片:哈希槽
集群将所有数据划分为 16384 个槽 ,每个键通过 CRC16(key) % 16384 映射到槽,槽再分配给各主节点。
c
// cluster.c: keyHashSlot
// 支持哈希标签: 若 key 含 {...}, 只对大括号内内容计算哈希
// 例如 {user1000}.following 与 {user1000}.followers 必落同一槽 → 支持多键操作
unsigned int keyHashSlot(char *key, int keylen) {
// 找到第一个 { 与之后的第一个 }, 对该子串做 CRC16
}
为什么是 16384 个槽?(高频追问)
- 节点间心跳包(PING/PONG)要携带自身的槽位图(bitmap),16384 位 = 2KB,可接受;若是 65536 位则心跳包过大;
- 官方建议集群规模不超过 1000 节点,16384 个槽足够分配(平均每节点至少 16 个槽);
- 槽数量压缩后心跳包更小,Gossip 通信开销可控。
(2)Gossip 协议与集群状态
节点间通过 MEET(握手)、PING/PONG(每秒随机选节点互发,携带自身状态与部分已知节点信息)、FAIL(广播某节点下线)等消息交换集群拓扑。每个节点维护 clusterState,所有槽都有主节点负责时为 ok。
(3)客户端路由:MOVED 与 ASK
- 客户端可向任意节点发请求;若键不在该节点,返回
MOVED <slot> <ip:port>→ 客户端更新本地槽映射缓存并重定向到正确节点; - 槽迁移过程中,源节点对迁移中的键返回
ASK重定向 → 客户端先发ASKING再发命令到目标节点,不更新本地缓存(因为迁移未完成,键可能还在源端)。
(4)故障检测与转移
- 集群内节点互相 PING,超时未回复则标记
PFAIL(疑似下线); - 当超过半数主节点 都将某主节点标记 PFAIL → 该节点被标记
FAIL(客观下线)并广播; - 故障转移与哨兵类似:下线主节点的从节点发起选举(需获得半数以上主节点投票),晋升为新主并接管原槽位,广播
PONG通知全集群。
(5)槽迁移(resharding)
redis-cli --cluster reshard 在线迁移槽:逐个把槽内的键从源节点 MIGRATE 到目标节点,迁移期间通过 ASK 重定向保证可用性。
2.8.3 关键细节与易错点
- 多键操作限制 :
MGET、事务、Lua 涉及多个键时,所有键必须在同一槽,否则报错------用哈希标签强制同槽,但要防止数据倾斜; - 客户端必须实现重定向:JedisCluster/Lettuce 已内置槽缓存与重定向,裸用 Jedis 连集群会失败;
- 集群不支持跨槽的 SELECT 多数据库:只有 db0;
- 发布订阅在集群中是全集群广播:消息会发到所有节点,注意流量放大;
- 扩容加节点后需手动/自动 rebalance:新节点默认无槽,不 rebalance 则不承载数据;
- 集群模式下哨兵不再需要:Cluster 自带故障转移,二者不叠加使用。
2.8.4 优劣势与能力边界
- 优势:水平扩展内存与写能力、自动故障转移、官方原生支持;
- 缺陷:多键/事务受槽约束、客户端复杂度高、运维(扩缩容、迁移)成本高于哨兵、异步复制仍有丢数据窗口;
- 适用:数据量大(>单机内存)或写 QPS 高的中大型业务;
- 不适用:小数据量场景(哨兵更简单);需要强一致的场景(考虑带 Raft 的方案或业务层幂等)。
2.9 事务与 Lua 脚本
2.9.1 子知识点概述
Redis 事务(MULTI/EXEC)与 Lua 脚本都用于"批量命令的原子执行 ",但能力差异显著:事务不支持回滚、命令错误分两类处理 ;Lua 脚本以单命令的原子性执行任意逻辑,是事实上的首选。
2.9.2 完整运行原理
(1)事务的三个阶段
redis
MULTI -- 开启事务, 后续命令入队
SET k1 v1
SET k2 v2
EXEC -- 原子执行队列中所有命令
- 入队阶段:命令不执行,仅放入客户端事务队列;
- 执行阶段 :EXEC 后按顺序执行,期间不会插入其他客户端命令(单线程保证隔离性);
- 错误处理两类 :
- 语法/编译错误 (如命令拼错):EXEC 前即报错,整个事务被丢弃;
- 运行时错误 (如对 String 执行 LPUSH):仅该命令失败,其余命令照常执行,不回滚。
(2)WATCH 乐观锁
redis
WATCH balance -- 监视键
val = GET balance
MULTI
SET balance (val - 100)
EXEC -- 若 balance 在 WATCH 后被修改, EXEC 返回 nil(事务失败)
WATCH 实现 CAS:被监视键在 EXEC 前被任何客户端修改,事务自动失败,客户端可重试------这是 Redis 实现乐观锁的唯一途径。
(3)Lua 脚本
redis
EVAL "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" 1 lock_key unique_id
- Redis 将 EVAL 的脚本当作单个命令执行,期间独占主线程,天然原子;
- 脚本通过
SCRIPT LOAD+EVALSHA复用,减少网络传输; - 脚本要短平快 :长脚本会阻塞所有请求,可用
SCRIPT KILL终止(仅限未产生写副作用时); - 集群限制:Lua 中操作的键必须在同一槽(哈希标签解决)。
(4)Redis 7.0 Function
Function 是 Lua 脚本的工程化升级:随库持久化、可复制、支持声明只读(#!lua flags=no-writes),解决 EVALSHA 脚本丢失问题,是 7.0+ 的推荐方案。
2.9.3 关键细节与易错点
- Redis 事务不支持回滚:官方认为命令失败多是编程错误,回滚不解决根本问题,且保持简单高性能;
- DISCARD 取消事务 、
UNWATCH清除监视; - 事务中不能使用查询命令的结果决定后续命令:入队时命令已固定,需要条件逻辑请用 Lua;
- Lua 脚本的随机性命令限制 :脚本中调用
TIME、RANDOMKEY等非确定性命令后再写,复制行为受限(4.0+ 的 effects replication 已放宽); - EVAL 的 numkeys 参数:必须正确声明 KEYS 数量,否则参数错位。
2.9.4 优劣势与能力边界
- 事务:适合"固定命令序列的原子提交",不适合有条件分支、需要回滚的复杂业务;
- Lua:适合原子化的复合逻辑(解锁、限流、库存扣减),但脚本复杂度上升后可读性/可维护性下降;
- 边界:涉及多服务、多数据源的分布式事务,Redis 事务/Lua 无能为力,需 Saga/TCC 等方案。
2.10 发布订阅与 Stream
2.10.1 子知识点概述
Redis 提供两种消息能力:Pub/Sub (广播式、无持久化、即发即弃)与 Stream(5.0+,支持持久化、消费者组、ACK、消息回溯),后者使 Redis 具备轻量级消息队列能力。
2.10.2 完整运行原理
(1)Pub/Sub
redis
SUBSCRIBE channel:news -- 订阅
PUBLISH channel:news "hello" -- 发布, 返回收到消息的订阅者数
- 消息不存储:订阅者离线期间的消息永久丢失;
- 支持模式订阅
PSUBSCRIBE channel:*; - 集群模式下消息全节点广播;
- 适用:配置变更通知、轻量事件广播;不适合可靠消息。
(2)Stream
redis
XADD mystream * field1 value1 -- 追加消息, * 表示自动生成 ID(毫秒-序号)
XREAD COUNT 10 STREAMS mystream 0 -- 从头读取
XGROUP CREATE mystream mygroup $ MKSTREAM -- 创建消费者组, $ 表示从最新消息开始
XREADGROUP GROUP mygroup consumer1 COUNT 10 STREAMS mystream > -- 组内消费, > 表示新消息
XACK mystream mygroup <message-id> -- 确认消息, 从 PEL 移除
XPENDING / XCLAIM -- 查询/转移超时未确认消息
核心机制:
- 消费者组 :组内消息竞争消费(一条消息只被组内一个消费者处理),不同组各自独立消费全量消息;
- PEL(Pending Entries List) :记录已投递未 ACK 的消息,消费者崩溃后可
XCLAIM转移给其他消费者------实现可靠投递; - 消息持久化 :Stream 存于内存并随 RDB/AOF 持久化,支持
MAXLEN裁剪长度。
2.10.3 关键细节与易错点
- Pub/Sub 的客户端输出缓冲 :订阅者消费慢会导致缓冲区膨胀,可能触发
client-output-buffer-limit pubsub强制断开; - Stream 的消息 ID 单调递增 ,
*自动生成依赖服务器时间,时钟回拨会报错(用*时 Redis 保证不回退); - XREADGROUP 阻塞读 :
BLOCK 0可长轮询,但要注意客户端连接管理; - Stream 不能替代专业 MQ:无分区水平扩展(单键单实例)、吞吐低于 Kafka,适合中小规模、不想引入额外中间件的场景。
2.10.4 优劣势与能力边界
- Pub/Sub:极简、低延迟,但不可靠;
- Stream:轻量可靠、零额外组件,适合订单通知、日志收集等中小流量场景;
- 边界:高吞吐、多租户、复杂路由的消息场景仍应选 Kafka/RocketMQ。
三、实战最佳实践
3.1 业务使用案例集
案例一:接口热点数据缓存 + 防穿透/击穿(电商商品详情)
-
业务背景:商品详情页 QPS 峰值 5 万,直接查 MySQL 导致数据库 CPU 打满;同时存在爬虫查询不存在商品 ID 的穿透风险。
-
实现目标:热点商品走 Redis,命中率 >95%;不存在的 ID 被拦截;热点键过期不击穿。
-
前置准备 :Spring Boot +
spring-boot-starter-data-redis(Lettuce)、布隆过滤器(RedissonRBloomFilter或 RedisBloom 模块)、Jackson。 -
实现步骤:
- 服务启动时把全量商品 ID 载入布隆过滤器;
- 请求先过 BF,再查缓存;
- 缓存未命中用互斥锁重建,空结果缓存 60 秒;
- 缓存 TTL 加随机抖动防雪崩。
-
核心代码实现:
java
public ProductVO getProduct(Long id) {
// 1. 参数校验 + 布隆过滤器拦截穿透
if (id == null || id <= 0) return null;
if (!productBloomFilter.contains(id)) { // BF 判定不存在, 直接返回
return null;
}
String cacheKey = "product:" + id;
String json = redisTemplate.opsForValue().get(cacheKey);
if (json != null) {
return "".equals(json) ? null : JSON.parseObject(json, ProductVO.class); // 空对象标记
}
// 2. 缓存未命中: 互斥锁防击穿
String lockKey = "lock:product:" + id;
String uuid = UUID.randomUUID().toString();
try {
if (tryLock(lockKey, uuid, 10)) { // SET NX EX 10
json = redisTemplate.opsForValue().get(cacheKey); // double check
if (json != null) return JSON.parseObject(json, ProductVO.class);
ProductVO vo = productMapper.selectById(id); // 查 DB
int ttl = 1800 + ThreadLocalRandom.current().nextInt(300); // 随机抖动防雪崩
if (vo == null) {
redisTemplate.opsForValue().set(cacheKey, "", 60, TimeUnit.SECONDS); // 缓存空值
} else {
redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(vo), ttl, TimeUnit.SECONDS);
}
return vo;
} else {
Thread.sleep(50); // 未获锁: 短暂等待后重试
return getProduct(id);
}
} finally {
unlock(lockKey, uuid); // Lua 原子解锁
}
}
- 效果验证 :压测对比开启前后 DB QPS(应从 5 万降至 <2000);
redis-cli INFO stats观察keyspace_hits/(hits+misses)命中率;故意查询不存在 ID 验证被 BF 拦截。 - 重点注意事项:BF 需随商品上新增量更新;互斥锁的 TTL 要大于 DB 查询最坏耗时;空值缓存 TTL 要短,防止新商品上架后被长时间误判为不存在。
案例二:秒杀库存扣减(Lua 原子操作)
-
业务背景:秒杀场景下库存扣减必须保证不超卖,数据库行锁在高并发下成为瓶颈。
-
实现目标:库存扣减原子化、不超卖、高性能(单实例 10 万+ QPS)。
-
前置准备 :预热阶段把库存写入
stock:{skuId};Lua 脚本预加载。 -
实现步骤:
- 活动开始前
SET stock:1001 500; - 请求执行 Lua 脚本:校验库存充足则扣减并返回 1,否则返回 0;
- 扣减成功的请求才进入后续下单流程(发 MQ 异步创建订单);
- 活动结束后库存回写数据库。
- 活动开始前
-
核心代码实现:
lua
-- decr_stock.lua: 原子扣减库存
local stock = tonumber(redis.call('get', KEYS[1])) -- 读取当前库存
if stock == nil then
return -1 -- 库存键不存在(未预热)
end
if stock <= 0 then
return 0 -- 已售罄, 快速失败
end
redis.call('decr', KEYS[1]) -- 扣减 1
return 1 -- 扣减成功
java
public boolean tryDecrStock(Long skuId) {
DefaultRedisScript<Long> script = new DefaultRedisScript<>(LUA_SCRIPT, Long.class);
Long ret = redisTemplate.execute(script,
Collections.singletonList("stock:" + skuId)); // KEYS[1]
return ret != null && ret == 1L;
}
- 效果验证 :JMeter 并发 1 万请求压测,验证
GET stock:1001最终值 = 初始库存 − 成功请求数,绝不出现负数;对比数据库行锁方案,QPS 提升 10 倍以上。 - 重点注意事项 :Lua 脚本保持短小,禁止在脚本中执行 O(N) 命令;集群模式下库存键与用户限购键需用哈希标签归入同一槽;扣减成功但下单失败时要有库存回补机制(事务消息或本地消息表保证最终一致)。
案例三:订单超时自动关闭(ZSet 延迟队列)
-
业务背景:用户下单后 30 分钟未支付需自动关闭订单并释放库存。定时扫表方案延迟高且扫全表压力大。
-
实现目标:秒级精度的延迟触发、不重复消费、可水平扩展。
-
前置准备:下单时向 ZSet 写入"到期时间戳 + 订单号";多个消费实例用分布式锁或分片竞争消费。
-
实现步骤:
- 下单成功:
ZADD delay:order <now+1800s 的毫秒时间戳> <orderId>; - 支付成功:
ZREM delay:order <orderId>移除; - 消费线程轮询取出到期元素,原子摘除后执行关单逻辑。
- 下单成功:
-
核心代码实现:
lua
-- pop_expired.lua: 原子取出并删除到期任务, 防止多消费者重复消费
local items = redis.call('zrangebyscore', KEYS[1], '0', ARGV[1], 'LIMIT', 0, ARGV[2])
if #items > 0 then
redis.call('zrem', KEYS[1], unpack(items)) -- 取到即删, 独占消费
end
return items
java
@Scheduled(fixedDelay = 500) // 每 500ms 轮询一次
public void pollExpiredOrders() {
long now = System.currentTimeMillis();
List<String> orderIds = redisTemplate.execute(POP_SCRIPT,
List.of("delay:order"), String.valueOf(now), "100");
for (String orderId : orderIds) {
orderService.closeIfUnpaid(orderId); // 关单前二次校验支付状态(幂等)
}
}
- 效果验证:构造测试订单不支付,观察 30 分钟 ±1 秒内订单状态流转为"已关闭"且库存回补;多实例部署下通过日志确认同一订单只被一个实例处理。
- 重点注意事项 :关单逻辑必须幂等(支付回调与关单任务可能竞争);轮询间隔决定精度,业务要求高可缩短间隔或分片多键;超大规模场景(亿级延迟任务)应评估 RocketMQ 延迟消息或时间轮方案,Redis ZSet 单键容量与轮询开销有上限。
3.2 行业通用规范
(1)键设计规范
- 命名格式统一为
业务名:对象名:ID,如mall:product:1001,禁止含空格与特殊字符; - 键长度控制在 128 字节内,值控制在 10KB 内(超过即视为大 key 预警);
- 所有键必须设置 TTL(除明确需要常驻的数据),且 TTL 加随机抖动;
- 禁止使用
keys前缀保留字,避免与系统键冲突。
(2)命令使用规范
- 生产禁用
KEYS *、FLUSHALL、FLUSHDB,用SCAN渐进遍历,rename-command屏蔽高危命令; - 避免 O(N) 命令作用于大集合:
HGETALL/LRANGE 0 -1/SMEMBERS改用HSCAN/分页LRANGE/SSCAN; - 删除大 key 用
UNLINK(4.0+ 异步删除),禁用同步DEL; - 批量操作用
MGET/MSET、Pipeline 减少 RTT,单次 Pipeline 命令数控制在千级以内。
(3)内存与配置规范
- 必须配置
maxmemory(建议物理内存的 60%~70%,为 COW 与复制缓冲留余量)+ 明确的淘汰策略; - 开启混合持久化
aof-use-rdb-preamble yes,appendfsync everysec; - 单实例内存建议不超过 10~16GB,更大容量走 Cluster 分片。
(4)客户端与连接规范
- 必须使用连接池(Lettuce/JedisPool),设置合理的
maxTotal/maxIdle与超时; - 命令必须设置读超时,避免网络异常时线程永久阻塞;
- 客户端禁止在 Redis 中存储未序列化的对象,统一 JSON/Protobuf。
(5)监控规范
- 必监控指标:内存水位(
used_memory)、命中率(keyspace_hits)、阻塞客户端数(blocked_clients)、慢查询数(slowlog)、主从延迟(slave_repl_offset差值)、fork 耗时(latest_fork_usec); - 定期执行
redis-cli --bigkeys/MEMORY USAGE巡检大 key。
3.3 高频踩坑案例
踩坑一:大 key 引发集群抖动
- 错误现象:某分片周期性 CPU 100%、其他命令超时,监控显示单键 2GB(一个存了千万级成员的 Hash)。
- 核心原因 :大 key 的任何读写都是 O(N),一次
HGETALL阻塞主线程数秒;删除时同步DEL触发长时间阻塞;主从复制与 Cluster 迁移大 key 时流量放大。 - 修复方案 :拆分为多个小 key(
user:1000:friends:{0..N}按 ID 取模);删除改用UNLINK或HSCAN分批HDEL。 - 规避技巧 :上线前评审数据结构,值超 10KB、集合超 5000 元素即告警;定期
--bigkeys扫描。
踩坑二:KEYS * 打挂生产
- 错误现象 :运维执行
KEYS *统计数量,服务瞬间全部超时数十秒。 - 核心原因 :
KEYS是 O(N) 阻塞命令,千万级 key 遍历期间主线程无法处理任何请求。 - 修复方案 :立即停止;改用
SCAN 0 COUNT 100游标迭代,或离线分析 RDB 文件(rdb-tools)。 - 规避技巧 :
rename-command KEYS ""直接禁用;统计需求用INFO keyspace或DBSIZE。
踩坑三:缓存与数据库双写不一致
- 错误现象:修改商品价格后,页面长时间展示旧价格。
- 核心原因:采用"更新 DB + 更新缓存"策略,并发下两个请求交叉执行导致旧值覆盖新值;或缓存更新失败无补偿。
- 修复方案 :统一采用 Cache Aside 模式------读:先缓存后 DB 回填;写:先更新 DB,再删除缓存;对一致性要求高的加"延迟双删"(删缓存 → 更新 DB → 延迟 500ms 再删一次);终极方案是订阅 binlog(Canal)异步删缓存,保证最终一致。
- 规避技巧:缓存层永远定位为"可丢失的加速副本",以数据库为准;为所有缓存设置兜底 TTL。
踩坑四:maxmemory 配置缺失导致 OOM 被杀
- 错误现象:Redis 进程被操作系统 OOM Killer 杀掉,服务雪崩。
- 核心原因 :未设
maxmemory,数据无限增长撑爆物理内存;或设了但忽略了复制缓冲区、COW 的额外开销。 - 修复方案 :配置
maxmemory+allkeys-lru;监控used_memory_rss(含碎片与 COW)而非仅used_memory;开启activedefrag治理碎片。 - 规避技巧:容量规划预留 30% 余量;内存水位 80% 即告警扩容。
踩坑五:连接池耗尽引发应用侧雪崩
- 错误现象 :应用日志大量
Could not get a resource from the pool,接口全部超时,但 Redis 本身正常。 - 核心原因:慢命令/网络抖动导致连接归还变慢,连接池过小(默认 8)被瞬间耗尽,线程全部阻塞等待。
- 修复方案 :调大
maxTotal(按峰值并发估算);设置maxWaitMillis快速失败而非无限等待;排查并消除慢命令(slowlog get)。 - 规避技巧:连接池大小压测定标;接入熔断器,Redis 异常时快速降级走数据库限流。
四、分层面试题库
4.1 基础入门题
问:什么是 Redis?它有哪些核心特点?
标准答案:Redis 是一款开源的、基于内存运行并支持持久化的高性能 Key-Value 型 NoSQL 数据库,用 ANSI C 编写。核心特点有五点:一是纯内存操作,读写延迟微秒级,单机 QPS 可达 10 万+;二是支持 String、Hash、List、Set、ZSet 五大基础数据类型及 Bitmap、HyperLogLog、GEO、Stream 等扩展类型;三是提供 RDB 与 AOF 两种持久化方式,重启数据不丢失;四是支持主从复制、哨兵、Cluster 集群等高可用架构;五是单线程命令模型保证操作原子性,还支持事务、Lua 脚本、发布订阅等丰富特性。它常被用作缓存、分布式锁、排行榜、计数器和轻量消息队列。
问:Redis 支持哪些数据类型?分别有什么应用场景?
标准答案:五大基础类型:①String------最简单,用于缓存对象 JSON、计数器(INCR)、分布式锁、Session 共享;②Hash------字段级读写的字典,适合存储用户、商品等对象,比存 JSON 更省且可局部更新;③List------双向链表(底层 quicklist),支持两端推拉,用于消息队列、最新列表(如朋友圈时间线);④Set------无序去重集合,支持交并差运算,用于抽奖(SRANDMEMBER/SPOP)、共同好友、标签去重;⑤ZSet------带分值的有序集合,用于排行榜、延迟队列、滑动窗口限流。扩展类型:Bitmap 做签到/在线状态统计,HyperLogLog 做亿级 UV 基数统计(误差 0.81%),GEO 做附近的人,Stream 做可靠消息队列。
问:Redis 为什么这么快?
标准答案:五个原因:第一,完全基于内存操作,避免磁盘 I/O;第二,核心命令处理是单线程模型,没有锁竞争和上下文切换,命令执行天然原子;第三,使用 I/O 多路复用(epoll)的事件驱动架构,单线程即可处理海量并发连接;第四,底层精心设计的数据结构------SDS、跳表、压缩列表、渐进式 rehash 的哈希表等,针对场景做了极致优化;第五,Redis 6.0 引入多线程 I/O,把网络读写和协议解析交给多线程,命令执行仍单线程,进一步释放网络吞吐。综合起来就是"内存 + 单线程无锁 + 多路复用 + 高效数据结构"。
问:Redis 和 Memcached 有什么区别?
标准答案:六个维度:①数据类型:Memcached 只支持简单 String,Redis 支持五大类型加扩展类型;②持久化:Memcached 无持久化,重启数据全丢,Redis 有 RDB/AOF;③线程模型:Memcached 是多线程,Redis 命令执行单线程(6.0 起 I/O 多线程);④内存管理:Redis 用 jemalloc 并有对象共享、紧凑编码等优化,内存利用率更高;⑤高可用:Redis 原生支持主从、哨兵、集群,Memcached 无原生方案;⑥特性:Redis 支持事务、Lua、发布订阅。结论是绝大多数场景选 Redis,只有纯简单 KV 缓存且要榨取多核性能时才考虑 Memcached。
问:Redis 的持久化方式有哪些?RDB 和 AOF 怎么选?
标准答案:两种基础方式加一种组合。RDB 是某一时刻的全量内存快照,通过 BGSAVE fork 子进程生成二进制文件,优点是文件紧凑、恢复极快,适合备份与灾备,缺点是两次快照之间宕机会丢数据,fork 大实例时有抖动。AOF 以追加方式记录每条写命令,支持 always/everysec/no 三种刷盘策略,everysec 最多丢 1 秒数据,安全性高,缺点是文件大、恢复慢,可通过 AOF 重写压缩。生产环境推荐 4.0+ 的混合持久化:AOF 重写时前半段存 RDB 格式、后半段追加增量命令,兼顾恢复速度和数据安全。
问:Redis 的过期键是怎么删除的?
标准答案:Redis 采用"惰性删除 + 定期删除"组合策略。惰性删除是指任何命令访问一个键时,先检查它是否过期,过期就删除再返回,这样不浪费 CPU,但过期键可能长期占内存。定期删除是弥补:Redis 的时间事件每隔 100 毫秒执行一次 activeExpireCycle,从每个库的过期字典里随机抽取 20 个键检查,删除其中已过期的;如果过期比例超过 25% 就继续抽,但单次执行不超过时间上限(默认 25 毫秒),超时则下轮继续,保证不阻塞主线程。另外从库不会主动删除过期键,它等待主库同步 DEL 命令,以保证主从数据一致。
问:Redis 的内存淘汰策略有哪些?
标准答案:当内存达到 maxmemory 上限且写命令到来时触发淘汰,共八种:按候选范围分两类,allkeys 系列作用于所有键,volatile 系列只作用于设置了 TTL 的键。具体有:allkeys-lru(近似最近最少使用,通用缓存首选)、volatile-lru、allkeys-lfu(4.0+,按访问频率淘汰,适合热点数据)、volatile-lfu、allkeys-random、volatile-random、volatile-ttl(优先淘汰剩余存活时间最短的)、以及 noeviction(不淘汰,写操作直接报 OOM 错误)。实现上 Redis 用采样近似 LRU:随机取 5 个键比较空闲时间,配合淘汰池,效果接近严格 LRU 但开销极小。
问:什么是缓存穿透、击穿、雪崩?怎么解决?
标准答案:三者都是缓存失效导致流量打到数据库。穿透是查询根本不存在的数据,缓存和数据库都没有,请求每次都打到 DB,解法是缓存空对象(短 TTL)或前置布隆过滤器拦截非法 ID。击穿是单个热点键过期瞬间,海量并发同时查 DB,解法是互斥锁(只让一个线程重建缓存)或逻辑过期(不设 TTL,异步刷新)。雪崩是大面积键同时过期或缓存服务宕机,解法是过期时间加随机抖动、热点永不过期、多级缓存(本地缓存 + Redis)、高可用架构(哨兵/集群)加熔断限流降级。一句话区分:穿透查"没有的",击穿是"一个热点挂了",雪崩是"一大片都挂了"。
问:Redis 分布式锁怎么实现?要注意什么?
标准答案 :加锁用一条原子命令 SET lock_key unique_id NX EX 30:NX 保证互斥,EX 设置过期时间防止持锁者崩溃后死锁,unique_id 标识持有者防止误删别人的锁。解锁必须用 Lua 脚本原子地"先判断值是不是自己的再删除"。三个进阶问题:一是锁续期,业务执行时间不确定时用看门狗机制(Redisson 默认 30 秒锁、每 10 秒续期一次);二是可重入,Redisson 用 Hash 结构记录重入计数;三是主从切换可能丢锁,极端场景可评估 Redlock(向多数独立实例加锁),但更务实的做法是接受 AP 特性并用业务幂等兜底。
问:Redis 的高可用方案有哪些?
标准答案:三级演进。第一级主从复制:从节点复制主节点数据,实现读写分离和数据冗余,但主节点故障需人工切换。第二级哨兵 Sentinel:独立的哨兵进程集群监控主从健康,主节点客观下线后自动选举新主并通知客户端,解决自动故障转移,但写能力和容量仍受限于单机。第三级 Redis Cluster:数据分成 16384 个哈希槽分布到多个主节点,每个主节点带从节点,兼具数据分片、写能力扩展和自动故障转移,是大数据量场景的官方方案。选型原则:数据量单机放得下用哨兵,放不下或写压力高用 Cluster。
4.2 原理深挖题
问:Redis 单线程模型具体是怎么工作的?6.0 为什么引入多线程?
标准答案:Redis 基于 ae 事件库实现 Reactor 模型:主线程运行事件循环,通过 epoll 监听大量 socket 连接。可读事件触发时读取请求、解析 RESP 协议,命令进入执行器按顺序执行(单线程保证无并发竞争),执行结果写回客户端;同时穿插时间事件处理过期键、持久化等后台任务调度。单线程的瓶颈不在 CPU 而在网络 I/O------大并发下协议解析、读写 socket 消耗大量 CPU。所以 6.0 把网络 I/O 环节多线程化:主线程把已建立连接的读写、协议解析分发给 I/O 线程并行处理,但命令执行仍然回到主线程串行,既提升吞吐又保留原子性。注意多线程只用于网络,不用于命令执行,"Redis 是单线程"的说法针对的始终是命令执行模型。
问:详细讲讲渐进式 rehash?
标准答案 :Redis 的字典内部维护两张哈希表 ht0 和 ht1。当负载因子达到阈值(正常情况 used/size ≥ 1,BGSAVE 期间放宽到 5 以避免 COW 内存翻倍)触发扩容:分配大小为第一个大于 used×2 的 2 的幂的 ht1,然后不是一次性迁移,而是把 rehashidx 置 0,分两步渐进迁移:一是对字典的每次增删改查操作顺带迁移一个桶;二是后台定时任务每 100ms 花 1ms 迁移。迁移期间查询先查 ht0 再查 ht1,新键一律写入 ht1,保证 ht0 只减不增,最终迁移完成后释放 ht0、ht1 转正。这样把 O(N) 的一次性开销均摊到每次操作,避免主线程长时间阻塞。缩容同理。
问:ZSet 为什么用跳表而不用红黑树?
标准答案:跳表是多层链表结构:插入时从最高层头节点逐层查找落点,新节点层数以 1/4 概率逐层晋升(上限 32 层),查找、插入、删除平均 O(logN)。选它而非红黑树有四个原因:第一,范围查询是 ZSet 的核心场景(ZRANGEBYSCORE/ZRANGE),跳表定位到起点后沿底层链表顺序遍历即可,红黑树需要中序遍历且涉及父指针回溯,效率低;第二,实现简单,插入删除只需局部改指针,没有旋转重平衡的复杂逻辑,antirez 本人也说过跳表实现不比平衡树复杂且更易调试;第三,跳表的 span 跨度字段可以 O(logN) 计算元素排名(ZRANK),红黑树要额外维护子树大小;第四,通过调整晋升概率可以灵活权衡时间与空间。另外 ZSet 底层是"跳表 + 字典"双结构:字典提供 O(1) 的按成员查分值,跳表负责排序和范围操作,两者共享成员字符串指针。
问:RDB 的 fork + COW 原理?子进程为什么能看到一致的数据?
标准答案:BGSAVE 时主进程调用 fork 创建子进程。fork 之后父子进程共享同一份物理内存页,内核把页表项标记为只读。子进程遍历内存生成 RDB 文件,它看到的是 fork 瞬间的一致性快照。此期间主进程继续服务客户端,当某个内存页被写操作修改时,触发缺页中断,内核才把该页复制一份------这就是写时复制(Copy-On-Write):父进程用新页,子进程仍持有旧页。所以 RDB 的额外内存开销不是翻倍,而是与"快照期间被修改的页数量"成正比。这也解释了三个工程问题:为什么快照期间写流量越大内存峰值越高(COW 复制的页越多);为什么 fork 本身耗时与实例内存成正比(要复制页表);为什么建议在业务低峰做 BGSAVE。
问:AOF 重写是怎么工作的?重写期间的新写入怎么处理?
标准答案:AOF 文件随命令累积会膨胀(比如 1000 次 INCR 记了 1000 条),重写机制通过 fork 子进程、读取当前内存状态直接生成最精简的命令集来压缩文件------注意重写不读旧 AOF 文件。重写期间客户端的新写命令会同时进入两个缓冲:AOF 缓冲区(照常追加到旧 AOF 文件,保证重写失败不丢数据)和 AOF 重写缓冲区。子进程完成新文件写入后,主进程把重写缓冲区中积累的增量命令追加到新 AOF 文件,然后原子 rename 替换。为避免重写期间磁盘 I/O 竞争,主进程对旧 AOF 的 fsync 会降频为每 2 秒一次。Redis 7.0 改为 Multi Part AOF(base + incr + manifest),重写期间不再需要重写缓冲区,消除了收尾时的阻塞窗口。
问:Redis 的近似 LRU 是怎么实现的?和 LFU 有什么区别?
标准答案:严格 LRU 需要维护全局双向链表,内存开销大,Redis 用采样近似:每个键的 redisObject 里有 24 位 lru 字段记录访问时间。淘汰时随机采样 maxmemory-samples 个(默认 5)键,比较空闲时间,淘汰最旧的;3.0 起引入 16 个位置的淘汰池,新采样键与池中键比较替换,使近似效果非常接近严格 LRU。LFU 是 4.0 引入的,把 24 位拆成 16 位上次访问时间 + 8 位对数频率计数器:访问频率越高计数越难增长,且计数随时间衰减。两者区别在于:LRU 看"最近一次访问时间",无法区分"一直热的数据"和"刚被偶然访问一次的冷数据"------全表扫描这类一次性访问会污染 LRU,把真正的热点挤出去;LFU 看"访问频率",能识别真正的热点。所以有缓存污染风险的场景选 allkeys-lfu。
问:详细讲讲主从复制的 PSYNC2 流程?
标准答案 :分全量和增量两条路径。首次同步时从节点发送 PSYNC ? -1,主节点回复 +FULLRESYNC replid offset,然后 BGSAVE 生成 RDB,期间新的写命令存入复制缓冲区;RDB 发给从节点,从节点清空旧数据加载,主节点再把复制缓冲区的增量命令发过去,之后进入持续的命令传播阶段。断线重连时从节点发送 PSYNC <replid> <offset>:如果 replid 匹配且 offset 还在复制积压缓冲区(环形缓冲,默认 1MB)内,主节点返回 +CONTINUE 只补发缺失命令,代价极小;否则退化为全量。4.0 引入的 replid2 解决故障切换问题:从节点晋升为主时保留前任的 replid 作为 replid2,这样其他从节点重连时仍能部分重同步,避免切换引发全量风暴。工程上要把 repl_backlog 调大(按断线时长 × 写入速率估算),避免网络抖动就触发全量。
问:Sentinel 故障转移的完整过程?
标准答案:分监控、判定、选举、转移四步。监控:每个哨兵每秒向主从和其他哨兵发 PING。判定:某实例在 down-after-milliseconds 内没有效回复,该哨兵标记其主观下线 SDOWN;如果下线的是主节点,哨兵会询问其他哨兵,达到 quorum 个哨兵都认为下线则升级为客观下线 ODOWN------从节点和哨兵自己只会有主观下线。选举:哨兵之间用 Raft 算法选 Leader,获得半数以上且不少于 quorum 票者胜出,由它执行故障转移。转移:Leader 从从节点中选新主,标准依次是排除不健康的、slave-priority 数值小的优先、复制偏移量大的(数据最新)优先、runid 小的兜底;然后向新主发 SLAVEOF NO ONE,让其他从节点复制新主,最后通过发布订阅通知客户端新地址。还要注意脑裂风险:网络分区时旧主可能被降级后仍以主身份写,可配 min-replicas-to-write 缓解。
问:Cluster 的哈希槽是怎么工作的?为什么是 16384 个?MOVED 和 ASK 有什么区别?
标准答案 :集群把数据分成 16384 个槽,每个键用 CRC16(key) % 16384 映射到槽,槽分配给各主节点;如果键包含 {...} 哈希标签,只对大括号内的内容计算哈希,可以让相关键强制落同一槽以支持多键操作。选 16384 的原因:节点间心跳包要携带自己的槽位图 bitmap,16384 位是 2KB 可接受,65536 位就是 8KB 心跳包太大;官方建议集群不超过 1000 个节点,16384 个槽足够分(平均每节点至少 16 个)。路由方面:客户端可以向任意节点发请求,键不归该节点管时返回 MOVED 并带目标地址,客户端更新本地槽缓存后重定向------这是永久性的;槽迁移过程中返回 ASK 重定向,客户端先发 ASKING 再发命令到目标节点,但不更新本地缓存,因为迁移未完成、键的归属未定。
问:Redis 事务为什么不支持回滚?WATCH 的原理?Lua 为什么是原子的?
标准答案:三个问题分开答。事务不回滚:Redis 认为命令运行时失败(如对 String 执行 LPUSH)通常是编程错误,是客户端该发现的问题,回滚掩盖不了错误;且不做回滚让事务实现保持简单、执行保持高效,所以事务中某条命令失败,其余命令照常执行。但注意编译期错误(命令拼错)会导致整个事务被丢弃。WATCH 原理:WATCH 把键加入客户端的监视列表,EXEC 时服务器检查被监视键自 WATCH 后是否被修改过,修改过则 EXEC 返回 nil 事务失败,客户端可重试------这是 CAS 乐观锁实现。Lua 原子性:Redis 把 EVAL 的整个脚本当作一条命令,在主线程的事件循环中执行,执行期间不会穿插任何其他客户端的命令,所以脚本内的多条命令对外表现为原子操作;代价是长脚本会阻塞所有请求,脚本必须短平快。
4.3 场景题
问:线上 Redis 突然变慢,所有命令延迟飙升,如何排查?
解题思路与方案 :按"先看监控、再定位命令、后查资源"三步走。第一步看监控面板:CPU 是否打满、内存水位、是否有主从全量同步在进行、fork 耗时(latest_fork_usec)是否异常------BGSAVE 的 fork 和 COW 是常见抖动源。第二步用 slowlog get 20 查慢查询,定位是否有 O(N) 命令(KEYS、HGETALL 大 Hash、SORT 大集合);用 redis-cli --bigkeys 或离线分析 RDB 找大 key,大 key 的任何操作都会阻塞主线程。第三步查 INFO clients 看是否有输出缓冲区堆积、INFO persistence 看 AOF fsync 是否卡磁盘(aof_delayed_fsync)、INFO replication 看是否在做大流量同步。对应处理:慢命令改写或拆分、大 key 拆分并 UNLINK 删除、把 BGSAVE 挪到低峰、磁盘换 SSD。最后用 redis-cli --latency 持续采样验证恢复。
问:如何保证缓存与数据库的双写一致性?
解题思路与方案:先明确缓存的定位是"可丢失的加速副本",追求的是最终一致而非强一致。标准方案是 Cache Aside 模式:读时先读缓存,未命中读 DB 回填;写时先更新数据库,再删除缓存(而不是更新缓存)。为什么删而不是更新:并发写时更新缓存可能出现旧值覆盖新值,删除则让下次读自然重建。为什么先更 DB 再删缓存:先删缓存再更 DB 的窗口期内,读请求会把旧值重新回填。对一致性要求更高的场景加两层保险:一是延迟双删------删缓存、更新 DB、延迟几百毫秒再删一次缓存,覆盖窗口期回填的旧值;二是订阅 binlog(Canal)异步删除缓存,即使应用侧删除失败也有兜底。所有缓存必须设置兜底 TTL,保证最坏情况下最终一致。金融级强一致场景不要依赖缓存,直接读库加锁。
问:设计一个日活 10 亿级的用户签到统计系统?
解题思路与方案 :核心是选对数据结构。方案一 Bitmap:每个用户每天一个位,SETBIT sign:{userId}:{yyyyMM} (day-1) 1,单用户单月仅 4 字节,查当月签到天数用 BITCOUNT,连续签到用位运算(把多天的 bitmap AND 后数 1)。10 亿用户按月分键,单键很小,无大 key 问题。方案二如果只关心"总签到人数"这类基数统计,用 HyperLogLog,12KB 存亿级基数,误差 0.81%。容量与扩展:按用户 ID 哈希分片到 Cluster 多个槽;月初自动滚动新键、旧键设 TTL 归档。性能优化:签到是写多读少,可先在应用层本地聚合再批量 SETBIT;统计报表类查询走离线(RDB 导出到数仓),不打在线库。这个方案比用 MySQL 存签到记录省两个数量级的存储。
问:Redis 集群需要在线扩容,如何平滑迁移?
解题思路与方案 :以 3 主 3 从扩到 4 主 4 从为例。第一步:新节点以空节点身份加入集群(redis-cli --cluster add-node),与现有节点握手同步拓扑。第二步:给新主节点挂一个从节点保证高可用。第三步:执行 reshard(redis-cli --cluster reshard),把部分槽从旧主迁移到新主:迁移以槽为单位,槽内键逐个 MIGRATE,迁移中通过 ASK 重定向保证读写不中断;建议低峰执行并限速(pipeline 参数),每迁完一个槽用 CLUSTER SETSLOT 确认归属。第四步:观察 CLUSTER NODES 确认槽分布均衡、INFO memory 各节点水位接近,最后验证业务读写正常。回滚预案:迁移中途出问题可以把槽迁回原主。关键注意点:迁移期间避免大 key(MIGRATE 是同步阻塞的)、客户端必须支持重定向、扩容后记得给新主补从节点。
问:设计一个秒杀系统,Redis 在其中承担什么角色?
解题思路与方案 :秒杀的流量模型是瞬时洪峰、绝大多数请求注定失败,设计原则是"把请求尽量挡在上游"。分层拦截:前端按钮置灰防重复提交 → 网关按用户限流(Redis + Lua 的令牌桶)→ 应用层本地缓存标记"已售罄"直接拒绝后续请求。库存核心:活动前预热 SET stock:{sku} N,请求用 Lua 脚本原子判断并扣减(库存 ≤0 快速返回),扣减成功才发 MQ 异步创建订单------Redis 挡住 99% 的无效流量,数据库只处理真实成交。防超卖靠 Lua 原子性,防重复下单靠 SETNX user:{uid}:{sku} 1。兜底:库存扣减成功但下单失败要回补(事务消息);数据库层用乐观锁(UPDATE ... WHERE stock > 0)做最终防线。容量上热点库存键是典型热 key,可拆成多个分片库存(stock:sku:0~7)分散到不同分片。
问:Redis 发生 OOM,如何排查和恢复?
解题思路与方案 :先看日志确认 OOM 类型------是 Redis 报 OOM command not allowed(达到 maxmemory 且 noeviction)还是进程被系统 OOM Killer 杀掉(物理内存耗尽)。前者查 INFO memory:used_memory 是否贴近 maxmemory,keyspace 键数量是否异常增长------定位是数据自然增长还是键没设 TTL 导致堆积,用 --bigkeys 找大 key,处理是清理无主键、设置合理淘汰策略、扩容。后者更隐蔽:used_memory 可能不高,但 used_memory_rss 很高,说明是内存碎片(开 activedefrag yes)或 fork 的 COW 放大(BGSAVE 期间写流量大)或复制缓冲区堆积(client-output-buffer-limit replica 配置过大、从节点过多),处理是错峰持久化、收紧缓冲区配置、减少从节点数量。恢复流程:若进程已被杀,优先从 RDB/AOF 恢复数据重启实例;若开启了混合持久化,恢复速度最快。恢复后立即检查 maxmemory 是否配置、淘汰策略是否合理,补上监控告警(内存水位 80% 告警),防止再次发生。根本治理:容量规划时预留 30% 余量给 COW 和碎片,maxmemory 设为物理内存的 60%~70%。
问:如何用 Redis 实现一个可靠的延迟队列?和 MQ 延迟消息怎么选型?
解题思路与方案 :Redis 方案用 ZSet:下单时 ZADD delay:order <到期时间戳> <orderId>,消费端轮询 ZRANGEBYSCORE delay:order 0 <now> LIMIT 0 N 取到期任务,用 Lua 脚本原子"取出并删除"防止多消费者重复消费,然后执行关单逻辑。关键设计点:①消费必须幂等------支付回调和关单任务可能竞争,关单前二次校验订单状态;②轮询间隔决定精度,500ms~1s 是常见选择;③多实例部署时用分布式锁或按 orderId 哈希分片到多个 ZSet 键,避免所有实例抢同一个键;④ZSet 单键容量有上限,亿级任务要分片。与 MQ 延迟消息(RocketMQ)的选型:任务量在百万级以内、精度要求秒级、团队已有 Redis 基建,用 ZSet 方案简单可控;任务量亿级、需要精确的延迟等级、需要消息回溯和死信处理,用 RocketMQ 延迟消息更合适。Redis 方案的优势是轻量、延迟低、不引入新组件;劣势是没有原生的重试、死信、消息回溯能力,需要自行实现。
问:Redis 热点 key 导致单分片过载,怎么解决?
解题思路与方案 :热点 key 的本质是所有请求都打到同一个槽、同一个节点,Cluster 的分片机制无法分散。解决分三层:第一层,应用侧本地缓存------用 Caffeine/Guava Cache 在 JVM 内缓存热点数据,TTL 设 1~5 秒,绝大部分请求不出进程,Redis 压力降一到两个数量级;这是最优先、成本最低的方案。第二层,读写分离------热点 key 的读请求分散到多个从节点,主节点只承担写;适合读多写少的场景。第三层,key 拆分------把一个热点 key 拆成 N 个副本(如 hot:product:1001:{0..7}),读请求随机路由到某个副本,写请求同时更新所有副本或用 Lua 保证一致;这样流量被分散到多个槽、多个节点。拆分副本数根据 QPS 和单节点承载能力估算。辅助手段:用 redis-cli --hotkeys(需 LFU 策略)或 MONITOR 采样提前发现热点;接入层做请求合并(相同 key 的并发请求只发一次 Redis)。
问:设计一个基于 Redis 的分布式限流方案,支持滑动窗口和令牌桶两种模式。
解题思路与方案 :两种模式分别实现。滑动窗口:用 ZSet,member 是请求唯一 ID(如 UUID),score 是请求时间戳。每次请求:①ZREMRANGEBYSCORE key 0 <now-window> 清除窗口外的旧记录;②ZCARD key 统计窗口内请求数;③未超限则 ZADD key <now> <uuid> 并 EXPIRE key <window+1>;超限则拒绝。整个逻辑封装在一个 Lua 脚本里保证原子性。令牌桶:用 Hash 存两个字段------tokens(当前令牌数)和 last_refill(上次补充时间)。每次请求:①计算距上次补充经过的时间,按速率补充令牌(不超过桶容量);②令牌 ≥1 则扣减并放行,否则拒绝;同样用 Lua 脚本原子执行。集群模式下,限流键用哈希标签 {user:10001}:ratelimit 保证落同一槽。精度与性能权衡:滑动窗口精度取决于窗口大小和 ZSet 内存,高 QPS 场景可改用固定窗口计数器(INCR + EXPIRE)牺牲精度换性能;令牌桶适合允许突发流量的场景。兜底:Redis 不可用时降级为本地限流(Guava RateLimiter),保证服务不完全裸奔。
问:Redis 主从切换后,客户端如何感知新主?如何避免切换期间的数据丢失?
解题思路与方案 :客户端感知分两种架构。哨兵模式:客户端连接哨兵集群,订阅 +switch-master 频道;主节点故障转移完成后,哨兵通过该频道广播新主地址,客户端收到后重建连接池。Lettuce 和 Jedis 都有原生 Sentinel 客户端支持,自动完成这一过程。Cluster 模式:客户端维护本地槽缓存表,收到 MOVED 重定向后自动更新槽映射,无需额外感知机制。切换期间数据丢失的根源:主节点故障时,复制缓冲区中尚未同步到从节点的写命令会丢失(异步复制的固有缺陷)。缓解措施:①配置 min-replicas-to-write 1 + min-replicas-max-lag 10,主节点在至少一个从节点 10 秒内有响应时才接受写入,否则拒绝写------牺牲可用性换数据安全性;②repl-backlog-size 调大,减少断线重连后全量同步的概率;③业务层对关键写操作做幂等设计,切换后重试不产生副作用;④对数据一致性要求极高的场景(如金融),考虑同步复制方案(Redis 本身不支持强同步,可评估 Redis Enterprise 或改用数据库)。核心认知:Redis 复制是异步的,主从切换存在秒级数据丢失窗口,这是 AP 系统的固有取舍,架构设计时必须接受并在业务层兜底。