
2026-09-03 · 阅读 1
一次性学会Redis
一次讲透 Redis:为什么快、缓存三兄弟、从单机到集群
本文是《一次讲透 XSS 与 CSRF》《一次讲透 JWT 与双 Token 机制》的续篇。前两篇讲"身份",这一篇讲"速度与稳定"——Redis 为什么快、缓存穿透/击穿/雪崩怎么防、以及从单机到分布式集群的演进逻辑。缓存三兄弟这一节是我在真实面试里翻过车的点,所以写得最细。
0. 先回答最经典的开放题:Redis 为什么快?
三层答案,一层比一层深,面试官的追问基本停在第三层:
第一层:纯内存操作
数据全放内存,对比数据库走磁盘,差五个量级。这是最根本的原因——内存随机读是纳秒级,磁盘寻道是毫秒级。
第二层:单线程命令执行 + IO 多路复用
这是最容易被追问的一层:"单线程为什么还快?"
- 命令执行是单线程的:没有锁竞争、没有线程上下文切换的开销,不用考虑并发问题;
- IO 多路复用:一个线程同时监听几万个客户端连接,谁就绪就处理谁,不阻塞等待。
追问的接法:Redis 的瓶颈在内存和网络,不在 CPU——命令本身都是内存级的简单操作,CPU 根本不是短板,所以单线程不吃亏,反而省掉了锁和切换的成本。
第三层:高效的数据结构
SDS 字符串、跳表、哈希表……按场景选结构,复杂度都压得很低。
0.1 顺带一问:跳表是什么?
有序链表 + 多级索引。底层链表存全量数据,往上每层抽稀建立"高速公路",查找时从最高层往下跳——O(log n),空间换时间。Redis 的 zset 底层就是它。
对比红黑树:跳表范围查询更顺(找到起点顺着底层链表走就行),且实现简单得多。
1. Redis 在我项目里干什么:PV/UV 统计
| 指标 | 含义 | 一句话 | 命令 |
|---|---|---|---|
| PV | Page View 点击量 | 每次打开都 +1,数动作 | INCR article:pv:42 |
| UV | Unique Visitor 独立访客 | 同一个人刷 100 次 = 1,数人头 | SADD article:uv:42 <userId> |
海量 UV 进阶用 HyperLogLog:PFADD / PFCOUNT,12KB 就能数亿 UV,误差 0.81%。
数据流:
用户打开文章
│
├─► Redis: INCR pv(数动作) ┐ 两个都是原子操作
└─► Redis: SADD uv(数人头) ┘ 并发不丢数、不用加锁
│
└─► 定时任务:每隔几分钟批量落库 PG(削峰,数据库压力恒定)
为什么计数放 Redis 不直接 UPDATE 数据库? 至少两个理由:
- 原子性:
INCR是原子操作,100 个并发同时执行结果一定 +100,不丢数也不加锁; - 削峰:高频写全挡在内存里,定时批量落库,数据库压力不随流量暴涨。
经典追问:INCR article:pv:42 当前值是 9,100 个用户并发各执行一次 INCR,最终是多少?——109,一定不丢。因为 INCR 是原子的,这正是计数必须用它的原因。
2. 缓存三大病:穿透、击穿、雪崩
三个问题的共性:请求绕过了缓存,直接砸到数据库。区别在"缓存为什么没挡住":
| 病名 | 一句话 | 比喻 |
|---|---|---|
| 穿透 | 查的是缓存和数据库里都没有的数据,每次必 miss,每次都打到数据库 | 无中生有 |
| 击穿 | 一个热点 key 过期的一瞬间,上万个请求同时砸向数据库回填 | 一夫当关 |
| 雪崩 | 一大片 key 同时过期或 Redis 整个宕机,请求集体打到数据库 | 集体崩盘 |
2.1 穿透:三种防御
场景:攻击者拿着 id = -1 或随机 id 疯狂查询 100 万次,每次都穿透。
- 缓存空值:查不到也缓存一个空值(短 TTL,如 60s)——挡的是反复查同一个不存在 id 的形态;
- 布隆过滤器:把所有合法 id 提前放进去,查询先过过滤器,不存在直接拒绝——挡的是随机生成、每次不重样的 id。注意布隆的特性:说"不存在"是 100% 准的,说"存在"可能误判,所以放在最前面当门卫刚好;
- 入口参数校验:
id < 0这种直接拒绝,零成本。
2.2 击穿:互斥锁
场景:热搜第一的词条缓存恰好过期,那一秒几十万请求同时打到库。
- 互斥锁:miss 的瞬间只放第一个请求去查库回填缓存,其他请求短暂等待后重读缓存——像病房只进一个家属去取药;
- 热点 key 不设物理过期,改逻辑过期,由后台异步更新(下文 2.4 展开)。
2.3 雪崩:双病因、双药方
场景:双十一零点,大批商品缓存同时到期,数据库瞬间被打满。
病因有两个,药方对应两副:
- 病因 A:大片 key 同时过期 → 药方:过期时间加随机值,
TTL = 基础值 + random(0~5min)。原理:根因是写入时都用了相同的固定 TTL,到期时间挤在同一秒;加随机值把过期时间打散到一个时间窗口里,任何时刻只过期几个,不会集体崩; - 病因 B:Redis 整个宕机 → 药方:Redis 高可用(主从 + 哨兵/集群),挂了自动切,见第 3 节。
2.4 物理过期 vs 逻辑过期
热点 key 不设过期怎么实现?这里有个容易懵的概念:
- 物理过期:Redis 自带的 TTL(
SET key value EX 300),到期 Redis 直接删,下一次请求必 miss; - 逻辑过期:
SET时不带 EX(key 本身永生,TTL返回 -1),把expireAt时间戳嵌进 value 里。读的时候发现逻辑上过期了,先返回旧值,同时异步去刷新——用户永远读到数据,miss 彻底消失。
代价:短窗口内可能读到旧数据——对"热点数据"来说,这通常是划算的。
3. 从单机到集群:分布式部署与 Redis 高可用演进
3.1 先搞清:为什么会有"并发"和"分布式"?
我曾经疑惑:一个小系统一台服务器不就够了吗,哪来的并发?
关键认知:并发不是"多个服务器",而是"多个客户端"。以收银系统为例——每台收银机都是一个客户端,10 家门店 30 台收银机同时卖同一件商品,库存存在云端是共享的。超卖的本质:同一份库存被多个请求同时读到"剩 1 件",各自都判定"还能卖",各自扣减。这跟几台服务器无关,只要用户 > 1,并发就存在。
同理,单体应用也有天花板:
- 性能上限:一台机器的 CPU/内存/带宽是固定的,流量翻倍就撑不住;
- 单点故障:机器一挂,全站宕机,没有备份。
破局两条路:
- 垂直扩容:换更强的机器——贵、且有上限;
- 水平扩容(分布式部署):多加几台普通机器,前面放 Nginx 负载均衡把流量分摊——便宜、可无限加。
水平扩容立刻带来一个新问题:Session 存在 A 机,请求被负载均衡打到 B 机就查不到。解法要么粘性会话,要么 Session 上 Redis 共享——这正是我上一篇文章说 JWT 无状态适合分布式的原因:不查表,任何机器拿同一把密钥都能验签。两篇在这里接上了。
3.2 主从复制:先解决"数据只有一份"
一主多从:主库写,把数据同步给从库,从库对外读。
- 收益:读写分离分摊读压力 + 数据冗余(主库盘坏了从库还有一份数据);
- 遗留问题:主库挂了,没有自动切换,得人工爬起来改配置。
3.3 哨兵:再解决"主库挂了没人管"
哨兵(Sentinel)是独立的"监工进程":
- 监控:持续 ping 主库;
- 通知:主库失联,标记主观下线,多个哨兵都确认后判客观下线;
- 自动故障转移:从从库里选一个提升为新主库,其他从库改认新主——人工切换变成自动,通常几十秒内完成。
遗留问题:每台机器还是存着全量数据。内存不够时(比如数据涨到 32G,单机只有 16G),主从+哨兵都救不了。
3.4 集群 Cluster:数据分片,解决"单机内存上限"
Redis Cluster 的核心是哈希槽分片:
- 全部键空间切成 16384 个哈希槽;
- 对 key 算 CRC16 哈希再对 16384 取模,落到某个槽;
- 槽再分配给各个主节点——比如 3 主 3 从,每个主节点分约 5461 个槽;
- 数据按 key 拆散到多台机器,每台只存三分之一,内存上限 × 3,还可以继续加机器。
关键特性:去中心化——没有统一的"大脑节点",所有主节点互相通信(gossip 协议),客户端连任意节点都能被指路到正确的节点。集群自带主从+故障转移,哨兵都不用单独部署了。
3.5 决策路径:能单机,不集群
演进不是越靠右越好,每一级都在加成本和复杂度:
单机 Redis ──► 主从复制 ──► 哨兵 ──► 集群
够用就行 读压力大 怕主库挂 内存不够/写太大
集群的代价:运维复杂度陡增、多 key 操作受槽位限制、事务和 lua 脚本跨槽受限。个人项目和中小系统,单机 + 持久化 + 合理 TTL 就够了——这也是一种架构判断力,不是"没用过",是"不需要"。
3.6 面试官问"你用过 Redis 集群吗?"——没用过怎么答
诚实 + 认知边界 + 迁移能力,三段式:
"生产上没有直接运维过集群,我项目用的是单机 Redis。但我理解它的演进逻辑:主从解决读压力和数据冗余,哨兵解决自动故障转移,集群用 16384 个哈希槽做数据分片解决单机内存上限。我这个体量单机足够,所以没有为上集群而上集群——如果未来数据量或写入量到瓶颈,我知道该往哪个方向演进。"
比硬编"用过"强十倍:编下去追问 gossip 协议细节立刻露馅,而"知道边界 + 知道演进路径"展示的恰恰是架构判断力。
4. 30 秒面试话术
"Redis 快在三层:纯内存操作是根本,差磁盘五个量级;单线程命令执行加 IO 多路复用,没有锁竞争和线程切换,瓶颈在内存和网络不在 CPU,所以单线程不吃亏;再配上 SDS、跳表这些高效数据结构。我项目里用它做 PV/UV 统计:PV 用 INCR 数动作,UV 用 SADD 数人头,都是原子操作并发不丢数,再定时批量落库削峰。缓存三大病我都防过:穿透用空值缓存加布隆过滤器,击穿用互斥锁,雪崩用随机 TTL 打散过期时间加 Redis 高可用兜底。热点 key 我会用逻辑过期,key 永生、过期时间嵌在 value 里,异步刷新,消灭 miss。高可用我知道从主从、哨兵到集群的演进:主从管冗余,哨兵管自动切换,集群用 16384 个哈希槽分片解决单机内存上限。我项目体量单机够用,所以我判断暂时不需要集群。"
5. 总结记忆卡
- 为什么快三连:内存(根本)→ 单线程+IO 多路复用(追问点:瓶颈不在 CPU)→ 数据结构
- 跳表:有序链表 + 多级索引,O(log n),空间换时间,zset 底层
- PV/UV:INCR 数动作 / SADD 数人头,原子性 + 削峰落库
- 三兄弟一句话:穿透 = 无中生有(查不存在的),击穿 = 一夫当关(单个热点 key 过期),雪崩 = 集体崩盘(大片同时过期/Redis 挂了)
- 布隆特性:说"不存在"100% 准,说"存在"可能误判——所以它当门卫,不在就拒之门外
- 雪崩双病因:同时过期 → 随机 TTL 打散;Redis 宕机 → 高可用自动切
- 逻辑过期:key 永生 + expireAt 嵌 value + 异步刷新 = 永不 miss,代价是短暂旧数据
- 高可用演进:主从(冗余+读写分离)→ 哨兵(自动故障转移)→ 集群(16384 哈希槽分片破内存上限)
- 架构判断:能单机不集群——说清边界和演进路径,比编"用过"更加分
本文基于个人项目实战(Redis PV/UV 统计 + 缓存设计)与面试复盘整理,如有疏漏欢迎指出。系列前篇:《一次讲透 XSS 与 CSRF》《一次讲透 JWT 与双 Token 机制》。