03 - 缓存:高并发第一利器
缓存是高并发优化里性价比最高、见效最快的一招。核心思想只有一句话:把慢的、频繁访问的数据,放到快的地方,下次直接拿。
一句话先记住:缓存是用「空间」换「时间」,用「可能的不一致」换「极快的速度」。
3.1 为什么缓存这么快
不同存储介质的读取速度,差了好几个数量级:
CPU 缓存 : ~1 纳秒
内存 : ~100 纳秒 ← Redis 在这里
SSD 硬盘 : ~100 微秒 ← 数据库数据大多在这
网络往返 : ~500 微秒
机械硬盘 : ~10 毫秒
从内存读,比从磁盘读快成百上千倍。缓存的本质,就是把数据从「慢介质」搬到「快介质」。
3.2 多级缓存:层层拦截
缓存不是只有 Redis。一个请求从用户到数据库,沿途可以设多道缓存:
用户
│
▼
[浏览器缓存] ← 静态资源、HTTP 缓存头
│
▼
[CDN 缓存] ← 图片/视频/JS,就近返回
│
▼
[网关缓存] ← Nginx 缓存热点页面
│
▼
[应用本地缓存] ← 进程内内存(最快,但每台机器一份)
│
▼
[分布式缓存 Redis] ← 多台应用共享,容量大
│ 没命中才查库
▼
[PostgreSQL]
| 层级 | 特点 | 适合缓存什么 |
|---|---|---|
| 浏览器 / CDN | 离用户最近,最快 | 静态资源、不常变的内容 |
| 本地缓存 | 进程内,纳秒级,但每台一份、有一致性问题 | 超高频的小数据(配置、字典) |
| Redis | 多机共享、容量大 | 大部分热点数据(用户信息、商品详情) |
本地缓存 + Redis 组合是常见的「二级缓存」:先查本地,本地没有再查 Redis,Redis 没有再查库。
3.3 缓存的基本模式:旁路缓存(Cache-Aside)
最常用的读写模式,你一定要理解它:
【读数据】
1. 先查缓存
2. 命中 → 直接返回 (快)
3. 没命中 → 查数据库 → 写回缓存 → 返回
【写数据】
1. 更新数据库
2. 删除缓存(不是更新缓存!)
为什么写的时候是「删除缓存」而不是「更新缓存」?
- 删除简单、不易出错;下次读的时候自然会重新加载最新值
- 如果去更新缓存,两个并发写可能让缓存存进「旧值」,产生不一致
3.4 缓存一致性:最头疼的问题
数据库更新了,缓存还是旧的——这就是不一致。经典方案是「延迟双删」:
1. 删除缓存
2. 更新数据库
3. 稍等一会儿(比如 500ms)
4. 再删一次缓存 ← 删掉这期间可能被读进来的旧值
重要认知:缓存和数据库的强一致几乎做不到,我们追求的是「最终一致」。对一致性要求极高的数据(如账户余额),就别缓存,直接查库。
3.5 缓存三大经典坑
这三个问题几乎是面试必考,也是线上事故高发区。
坑一:缓存穿透 —— 查一个根本不存在的数据
恶意请求:查 id = -1 的用户(根本不存在)
→ 缓存没有(因为不存在)
→ 每次都穿透到数据库
→ 大量这种请求 → 数据库被打垮
对策:
- 把「不存在」也缓存起来(缓存一个空值,设短过期时间)
- 用布隆过滤器:先判断「这个 id 可能存在吗」,不可能存在的直接拦掉
坑二:缓存击穿 —— 一个热点 key 突然过期
某个超热门商品的缓存 22:00 过期
→ 这一瞬间成千上万请求同时发现「缓存没了」
→ 一起涌向数据库查同一条数据
→ 数据库瞬间被打崩
对策:
- 互斥锁:只让第一个请求去查库并重建缓存,其他请求等一下
- 热点数据不过期(或后台异步续期)
坑三:缓存雪崩 —— 大量 key 同时过期
系统重启时,一批缓存设了相同的过期时间(比如都 1 小时)
→ 1 小时后,成千上万 key 同时失效
→ 所有请求同时打到数据库
→ 数据库崩溃 → 引发连锁反应(雪崩)
对策:
- 过期时间加随机值(比如 1 小时 ± 随机几分钟),错开失效时间
- 缓存高可用(Redis 主从 / 集群),别让缓存整个挂掉
| 三大坑 | 一句话区别 | 核心对策 |
|---|---|---|
| 穿透 | 查不存在的数据 | 缓存空值 / 布隆过滤器 |
| 击穿 | 单个热点 key 失效 | 互斥锁 / 热点不过期 |
| 雪崩 | 大量 key 同时失效 | 过期时间加随机 / 缓存高可用 |
3.6 缓存的代价(别滥用)
- 一致性问题:缓存和数据库天然会有短暂不一致
- 额外复杂度:多了一套要运维的 Redis,多了「命中率、过期、淘汰」要操心
- 内存成本:内存比磁盘贵,不能什么都往里塞
什么该缓存:读多写少、能容忍短暂不一致、访问频繁的数据(用户信息、商品详情、配置)。 什么不该缓存:写极频繁、要求强一致、几乎不被重复读的数据。
本章小结
| 你要记住 | 要点 |
|---|---|
| 为什么快 | 内存比磁盘快成百上千倍 |
| 多级缓存 | 浏览器 → CDN → 本地 → Redis → DB,层层拦截 |
| 读写模式 | 旁路缓存:读时回填,写时删除缓存 |
| 一致性 | 追求最终一致,延迟双删;强一致数据别缓存 |
| 三大坑 | 穿透(查不存在)、击穿(单热点失效)、雪崩(大量同时失效) |
下一章预告:缓存解决了「读」的压力,那「写」和「慢任务」怎么办?答案是异步化 + 消息队列削峰。
上一章 ← 02 - 全链路瓶颈地图 | 下一章 → 04 - 异步化与削峰