Portfolio
← 返回博客列表
一次性学会Redis

2026-09-03 · 阅读 1

一次性学会Redis

Redis

一次讲透 Redis:为什么快、缓存三兄弟、从单机到集群

本文是《一次讲透 XSS 与 CSRF》《一次讲透 JWT 与双 Token 机制》的续篇。前两篇讲"身份",这一篇讲"速度与稳定"——Redis 为什么快、缓存穿透/击穿/雪崩怎么防、以及从单机到分布式集群的演进逻辑。缓存三兄弟这一节是我在真实面试里翻过车的点,所以写得最细。

0. 先回答最经典的开放题:Redis 为什么快?

三层答案,一层比一层深,面试官的追问基本停在第三层:

第一层:纯内存操作

数据全放内存,对比数据库走磁盘,差五个量级。这是最根本的原因——内存随机读是纳秒级,磁盘寻道是毫秒级。

第二层:单线程命令执行 + IO 多路复用

这是最容易被追问的一层:"单线程为什么还快?"

  1. 命令执行是单线程的:没有锁竞争、没有线程上下文切换的开销,不用考虑并发问题;
  2. IO 多路复用:一个线程同时监听几万个客户端连接,谁就绪就处理谁,不阻塞等待。

追问的接法:Redis 的瓶颈在内存和网络,不在 CPU——命令本身都是内存级的简单操作,CPU 根本不是短板,所以单线程不吃亏,反而省掉了锁和切换的成本。

第三层:高效的数据结构

SDS 字符串、跳表、哈希表……按场景选结构,复杂度都压得很低。

0.1 顺带一问:跳表是什么?

有序链表 + 多级索引。底层链表存全量数据,往上每层抽稀建立"高速公路",查找时从最高层往下跳——O(log n),空间换时间。Redis 的 zset 底层就是它。

对比红黑树:跳表范围查询更顺(找到起点顺着底层链表走就行),且实现简单得多。

1. Redis 在我项目里干什么:PV/UV 统计

指标含义一句话命令
PVPage View 点击量每次打开都 +1,数动作INCR article:pv:42
UVUnique 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 数据库? 至少两个理由:

  1. 原子性INCR 是原子操作,100 个并发同时执行结果一定 +100,不丢数也不加锁;
  2. 削峰:高频写全挡在内存里,定时批量落库,数据库压力不随流量暴涨。

经典追问:INCR article:pv:42 当前值是 9,100 个用户并发各执行一次 INCR,最终是多少?——109,一定不丢。因为 INCR 是原子的,这正是计数必须用它的原因。

2. 缓存三大病:穿透、击穿、雪崩

三个问题的共性:请求绕过了缓存,直接砸到数据库。区别在"缓存为什么没挡住":

病名一句话比喻
穿透查的是缓存和数据库里都没有的数据,每次必 miss,每次都打到数据库无中生有
击穿一个热点 key 过期的一瞬间,上万个请求同时砸向数据库回填一夫当关
雪崩一大片 key 同时过期或 Redis 整个宕机,请求集体打到数据库集体崩盘

2.1 穿透:三种防御

场景:攻击者拿着 id = -1 或随机 id 疯狂查询 100 万次,每次都穿透。

  1. 缓存空值:查不到也缓存一个空值(短 TTL,如 60s)——挡的是反复查同一个不存在 id 的形态;
  2. 布隆过滤器:把所有合法 id 提前放进去,查询先过过滤器,不存在直接拒绝——挡的是随机生成、每次不重样的 id。注意布隆的特性:说"不存在"是 100% 准的,说"存在"可能误判,所以放在最前面当门卫刚好;
  3. 入口参数校验id < 0 这种直接拒绝,零成本。

2.2 击穿:互斥锁

场景:热搜第一的词条缓存恰好过期,那一秒几十万请求同时打到库。

  1. 互斥锁:miss 的瞬间只放第一个请求去查库回填缓存,其他请求短暂等待后重读缓存——像病房只进一个家属去取药;
  2. 热点 key 不设物理过期,改逻辑过期,由后台异步更新(下文 2.4 展开)。

2.3 雪崩:双病因、双药方

场景:双十一零点,大批商品缓存同时到期,数据库瞬间被打满。

病因有两个,药方对应两副

  1. 病因 A:大片 key 同时过期 → 药方:过期时间加随机值TTL = 基础值 + random(0~5min)。原理:根因是写入时都用了相同的固定 TTL,到期时间挤在同一秒;加随机值把过期时间打散到一个时间窗口里,任何时刻只过期几个,不会集体崩;
  2. 病因 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,并发就存在。

同理,单体应用也有天花板:

  1. 性能上限:一台机器的 CPU/内存/带宽是固定的,流量翻倍就撑不住;
  2. 单点故障:机器一挂,全站宕机,没有备份。

破局两条路:

  • 垂直扩容:换更强的机器——贵、且有上限;
  • 水平扩容(分布式部署):多加几台普通机器,前面放 Nginx 负载均衡把流量分摊——便宜、可无限加。

水平扩容立刻带来一个新问题:Session 存在 A 机,请求被负载均衡打到 B 机就查不到。解法要么粘性会话,要么 Session 上 Redis 共享——这正是我上一篇文章说 JWT 无状态适合分布式的原因:不查表,任何机器拿同一把密钥都能验签。两篇在这里接上了。

3.2 主从复制:先解决"数据只有一份"

一主多从:主库写,把数据同步给从库,从库对外读。

  • 收益:读写分离分摊读压力 + 数据冗余(主库盘坏了从库还有一份数据);
  • 遗留问题:主库挂了,没有自动切换,得人工爬起来改配置。

3.3 哨兵:再解决"主库挂了没人管"

哨兵(Sentinel)是独立的"监工进程":

  1. 监控:持续 ping 主库;
  2. 通知:主库失联,标记主观下线,多个哨兵都确认后判客观下线;
  3. 自动故障转移:从从库里选一个提升为新主库,其他从库改认新主——人工切换变成自动,通常几十秒内完成。

遗留问题:每台机器还是存着全量数据。内存不够时(比如数据涨到 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 机制》。