Redis 缓存三剑客——穿透/击穿/雪崩 + 一致性策略 / Redis Cache Penetration, Breakdown, Avalanche, and Consistency
📅 创建时间:2026-06-02 🏷️ 标签:#Redis #缓存穿透 #缓存击穿 #缓存雪崩 #布隆过滤器 #分布式锁 #缓存一致性 #延时双删 📚 前置知识:[[00-redis-overview]](Redis 全景) [[01-redis-architecture]](架构分析) [[/03-web/06-databases-and-data-access/05-redis]](Redis 数据结构) 📚 相关知识:[[04-redis-distributed-locks]](分布式锁) [[/03-web/07-middleware/01-redis-deep]](Redis 三问已有内容,此文更系统) [[/03-web/07-middleware/05-cache-strategy]](缓存一致性策略)
场景:凌晨 2 点,你的系统被黑产打垮了
┌─────────────────────────────────────────────────────────────┐
│ │
│ 凌晨 2:15,你被报警电话叫醒。 │
│ │
│ 监控大屏: │
│ Redis 命中率:99% → 0% ↓↓↓ │
│ MySQL CPU: 5% → 100% ↑↑↑ │
│ 接口响应: 50ms → 超时 │
│ │
│ 错误日志清一色: │
│ SELECT * FROM products WHERE id = 9999999 │
│ SELECT * FROM products WHERE id = 8888888 │
│ SELECT * FROM products WHERE id = 7777777 │
│ │
│ 排查发现:有人用大量不存在的商品 ID,疯狂刷你的秒杀接口。 │
│ │
│ 这就是"缓存穿透"。 │
│ 但这只是缓存问题的冰山一角。 │
│ 这一章,我们系统性地理解缓存的三大杀手 + 一致性策略。 │
│ │
└─────────────────────────────────────────────────────────────┘第1节:缓存穿透——查询一个根本不存在的数据
问题抽象
┌─────────────────────────────────────────────────────────────┐
│ 缓存穿透的图书馆比喻 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 正常情况: │
│ 读者要书 → 先看展示柜(缓存)→ 有就拿走(命中) │
│ → 没有就进图书馆找(未命中)→ 找到后放一本到展示柜│
│ │
│ 恶意情况(穿透): │
│ 有人拿着一张根本不存在的书单来要书 │
│ → 展示柜没有 → 进图书馆找 → 图书馆也没有 │
│ → 不放入展示柜(因为没有这本书) │
│ → 下次同样的请求再来 → 再次进图书馆找 → 死循环 │
│ │
│ 核心问题: │
│ 不存在的数据,缓存层不存储 → 每次请求都穿透到数据库 │
│ 恶意刷大量不存在的 ID → 数据库被打垮 │
│ │
└─────────────────────────────────────────────────────────────┘推演:穿透的请求链路
┌─────────────────────────────────────────────────────────────┐
│ 穿透的请求链路 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 第一次请求 id=9999999: │
│ 用户请求 → 查 Redis(miss)→ 查 MySQL(无数据)→ 返回空 │
│ → Redis 不缓存空值 │
│ │
│ 第二次请求 id=9999999: │
│ 用户请求 → 查 Redis(miss)→ 查 MySQL(无数据)→ 返回空 │
│ ↑ │
│ 每次都走数据库! │
│ │
│ 10万个恶意ID × 每次都走数据库 = 10万次数据库查询 │
│ → 数据库爆炸 → 系统崩溃 │
│ │
│ 正常业务也可能穿透: │
│ • 商品下架后爬虫仍在请求该商品 ID │
│ • 用户输入了不存在的搜索词 │
│ │
└─────────────────────────────────────────────────────────────┘解法一:布隆过滤器(最优雅,推荐)
┌─────────────────────────────────────────────────────────────┐
│ 布隆过滤器原理 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 布隆过滤器 = 一个位数组 + N 个哈希函数 │
│ │
│ 存入 "商品ID=123": │
│ hash1(123) = 7 → 位数组[7] = 1 │
│ hash2(123) = 15 → 位数组[15] = 1 │
│ hash3(123) = 23 → 位数组[23] = 1 │
│ │
│ 查询 "商品ID=123是否存在": │
│ hash1(123) = 7 → 位数组[7] = 1 ✓ │
│ hash2(123) = 15 → 位数组[15] = 1 ✓ │
│ hash3(123) = 23 → 位数组[23] = 1 ✓ │
│ → 三次都是 1 → 认为存在(可能误判,但不漏报) │
│ │
│ 查询 "商品ID=9999999是否存在": │
│ hash1(9999999) = 7 → 位数组[7] = 1 ✓ │
│ hash2(9999999) = 2 → 位数组[2] = 0 ✗ │
│ → 有 0 → 一定不存在! │
│ │
│ 核心特性: │
│ • 说"不存在" → 一定不存在(不会有漏报) │
│ • 说"存在" → 可能不存在(误判率可调,~1%) │
│ • Redis 4.0+ 支持插件:BF.ADD / BF.EXISTS / BF.MADD │
│ │
└─────────────────────────────────────────────────────────────┘python
# Redis 4.0+ 原生布隆过滤器使用
import redis
r = redis.Redis()
# 启动时:把数据库中所有合法商品 ID 写入布隆过滤器
def init_bloom_filter():
product_ids = db.query("SELECT id FROM products")
pipe = r.pipeline()
for pid in product_ids:
pipe.execute_command("BF.ADD", "bloom:products", str(pid))
pipe.execute()
# 查询流程
def get_product(product_id):
# 第一关:布隆过滤器(毫秒级判断)
exists = r.execute_command("BF.EXISTS", "bloom:products", str(product_id))
if not exists:
return None # 一定不存在,直接返回,不查 DB
# 第二关:Redis 缓存
cache_key = f"product:{product_id}"
val = r.get(cache_key)
if val:
return json.loads(val)
# 第三关:数据库
product = db.query("SELECT * FROM products WHERE id = %s", product_id)
if product:
r.setex(cache_key, 3600, json.dumps(product))
return product解法二:缓存空值(最简单)
python
# 思路:不存在的值也缓存起来,但过期时间要短
def get_product(product_id):
cache_key = f"product:{product_id}"
val = r.get(cache_key)
if val:
if val == "NULL": # 空值的特殊标记
return None
return json.loads(val)
# 缓存 miss,查数据库
product = db.query("SELECT * FROM products WHERE id = %s", product_id)
if product is None:
r.setex(cache_key, 300, "NULL") # 空值缓存 5 分钟
else:
r.setex(cache_key, 3600, json.dumps(product)) # 正常缓存 1 小时
return product适用场景:数据"确实不存在"的时间窗口已知且可控(如商品下架)。
解法三:参数校验(最根本)
python
# 网关层/业务层:最基础的防线
def check_request(product_id):
if product_id < 0: return False # 负数 ID 不存在
if product_id > 10_000_000: return False # 超出商品 ID 范围
return True三层防御体系
┌─────────────────────────────────────────────────────────────┐
│ 缓存穿透三层防御体系 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 第一层:参数校验(最便宜) │
│ → 拦掉明显无效的请求(负数、超范围) │
│ │
│ 第二层:布隆过滤器(推荐) │
│ → 在 Redis 层面判断数据是否可能存在 │
│ → 不存在的请求直接返回,不碰数据库 │
│ │
│ 第三层:缓存空值(兜底) │
│ → 即使布隆过滤器误判(说存在但实际不存在) │
│ → 数据库查不到也缓存起来,下次直接命中 │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 请求进来 │ │
│ │ ↓ │ │
│ │ 参数校验:合法? │ │
│ │ ↓ 不合法 → 直接返回空 │ │
│ │ 布隆过滤器:可能存在? │ │
│ │ ↓ 不存在 → 直接返回空 │ │
│ │ Redis 缓存:命中? │ │
│ │ ↓ 未命中 → 查 DB → 缓存结果 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘第2节:缓存击穿——热点数据过期那一瞬间
问题抽象
┌─────────────────────────────────────────────────────────────┐
│ 缓存击穿的图书馆比喻 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 一本超级热门的书(热点数据),放在展示柜里只有一本。 │
│ 所有人都来借这本书。 │
│ │
│ 问题来了:这本书的"展示时间"(缓存过期)到了, │
│ 要拿回图书馆重新放一本书进去。 │
│ │
│ 就在这个交接的瞬间: │
│ 所有读者同时冲向图书馆 → 图书馆被挤爆了。 │
│ │
│ 核心问题: │
│ 大量请求集中在同一个 key 过期的那一刻, │
│ 同时穿透到数据库 → 数据库被击穿。 │
│ │
└─────────────────────────────────────────────────────────────┘真实场景:秒杀商品缓存过期
┌─────────────────────────────────────────────────────────────┐
│ 缓存击穿的真实场景 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 凌晨 0 点,秒杀商品 A 的缓存刚刚过期。 │
│ │
│ 10:00:00 - 100 个请求同时到达: │
│ 请求 1:查 Redis(过期了)→ 查 MySQL → 查到了 │
│ 请求 2:查 Redis(过期了)→ 查 MySQL → 查到了 │
│ 请求 3:查 Redis(过期了)→ 查 MySQL → 查到了 │
│ ...(100 个请求全部打到了数据库) │
│ │
│ 如果商品是爆款,可能 10 万个请求同时进来 │
│ → 10 万次数据库查询 → 数据库崩溃 │
│ │
│ 穿透 vs 击穿的区别: │
│ • 穿透:大量不同的 key 都不存在 │
│ • 击穿:同一个热 key 过期的瞬间大量请求同时涌入 │
│ │
└─────────────────────────────────────────────────────────────┘解法一:互斥锁(保守但稳妥)
┌─────────────────────────────────────────────────────────────┐
│ 互斥锁原理 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 第一个请求拿到锁 → 查数据库 → 写入缓存 → 释放锁 │
│ 其他请求等待锁 → 锁被释放 → 查 Redis(有了!)→ 直接命中│
│ │
│ 效果:只有 1 个请求查数据库,其他全部等锁后命中缓存 │
│ │
└─────────────────────────────────────────────────────────────┘python
import redis
import uuid
import time
import json
r = redis.Redis()
def get_product_with_lock(product_id):
cache_key = f"product:{product_id}"
val = r.get(cache_key)
if val:
return json.loads(val)
lock_key = f"lock:product:{product_id}"
lock_id = str(uuid.uuid4())
# SET NX EX:原子获取锁(key 不存在才设,并设过期时间)
if r.set(lock_key, lock_id, nx=True, ex=10):
try:
# 拿到锁:查数据库
product = db.query("SELECT * FROM products WHERE id = %s", product_id)
if product:
r.setex(cache_key, 3600, json.dumps(product))
return product
finally:
# 释放锁(Lua 脚本保证原子性)
lua = """
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
"""
r.eval(lua, 1, lock_key, lock_id)
else:
# 没拿到锁:等 50ms 后重试
time.sleep(0.05)
return get_product_with_lock(product_id)解法二:逻辑过期(推荐,热点数据)
python
import time
from threading import Thread
def get_product_logic_expire(product_id):
cache_key = f"product:{product_id}"
val = r.get(cache_key)
if not val:
return get_and_cache(product_id)
data = json.loads(val)
expire_time = data["_expire_at"]
if time.time() > expire_time:
# 逻辑过期:开启独立线程刷新缓存,不阻塞当前请求
Thread(target=refresh_cache, args=(product_id,)).start()
# 返回旧数据(短暂不一致可以接受)
return data
return data
def get_and_cache(product_id):
product = db.query("SELECT * FROM products WHERE id = %s", product_id)
if not product:
return None
data = {
**product,
"_expire_at": time.time() + 300 # 逻辑过期 5 分钟
}
r.setex(cache_key, 86400, json.dumps(data)) # 物理过期 24 小时
return product
def refresh_cache(product_id):
product = db.query("SELECT * FROM products WHERE id = %s", product_id)
if product:
data = {**product, "_expire_at": time.time() + 300}
r.setex(f"product:{product_id}", 86400, json.dumps(data))为什么推荐逻辑过期:
┌─────────────────────────────────────────────────────────────┐
│ 互斥锁 vs 逻辑过期 对比 │
├─────────────────────────────────────────────────────────────┤
│ │
│ │ 维度 │ 互斥锁 │ 逻辑过期 │
│ ├───────────────┼──────────────────┼──────────────────────┤
│ │ 原理 │ 只有一个线程查 DB │ 所有线程都能返回旧数据│
│ ├───────────────┼──────────────────┼──────────────────────┤
│ │ 响应时间 │ 等锁的线程慢 │ 所有线程都快速返回 │
│ ├───────────────┼──────────────────┼──────────────────────┤
│ │ 数据一致性 │ 一定是最新的 │ 短暂不一致(~100ms) │
│ ├───────────────┼──────────────────┼──────────────────────┤
│ │ 实现复杂度 │ 中 │ 低 │
│ ├───────────────┼──────────────────┼──────────────────────┤
│ │ 适用场景 │ 数据一致性要求高 │ 热点数据,允许短暂不一致│
│ │
│ 推荐:热点数据 → 逻辑过期;强一致数据 → 互斥锁 │
│ │
└─────────────────────────────────────────────────────────────┘解法三:热点数据永不过期(极端热点)
python
# 启动时预热:热点商品永不过期
def preload_hot_products():
hot_products = db.query("SELECT * FROM products WHERE is_hot = 1")
for product in hot_products:
r.set(f"product:{product['id']}", json.dumps(product))
# 不设 EX,永不过期
# 后台任务:每 30 分钟刷新一次
def refresh_hot_products():
while True:
hot_products = db.query("SELECT * FROM products WHERE is_hot = 1")
for product in hot_products:
r.set(f"product:{product['id']}", json.dumps(product))
time.sleep(1800)适用场景:双十一爆款等极端热点场景。代价是数据更新有延迟。
第3节:缓存雪崩——所有缓存同时失效
问题抽象
┌─────────────────────────────────────────────────────────────┐
│ 缓存雪崩的图书馆比喻 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 展示柜里所有书的"展示时间"都是同一个时刻设置的。 │
│ │
│ 灾难场景:凌晨 0 点,你设的"每日缓存过期时间"全部到期。 │
│ │
│ 一瞬间,所有读者同时冲向图书馆—— │
│ 不是一本书,是所有书同时过期—— │
│ 图书馆被挤垮了。 │
│ │
│ 核心问题: │
│ 大量不同的 key 在同一时刻同时过期, │
│ 所有请求同时穿透到数据库 → 数据库被打垮。 │
│ │
└─────────────────────────────────────────────────────────────┘真实场景:统一过期时间
python
# 很多项目的写法:统一 24 小时过期
def get_user(user_id):
user = db.query("SELECT * FROM users WHERE id = %s", user_id)
r.setex(f"user:{user_id}", 86400, json.dumps(user))
return user
# 问题:如果系统在 0 点大量用户注册/访问
# → 24 小时后凌晨 0 点,所有缓存同时过期
# → 所有请求同时穿透到数据库
# → 数据库瞬间压力暴增解法一:过期时间随机化
python
import random
def get_user(user_id):
cache_key = f"user:{user_id}"
val = r.get(cache_key)
if val:
return json.loads(val)
user = db.query("SELECT * FROM users WHERE id = %s", user_id)
if user:
# 过期时间加随机偏移量,避免同时失效
expire = 86400 + random.randint(0, 3600) # 24-25 小时随机
r.setex(cache_key, expire, json.dumps(user))
return user解法二:多级缓存(同时解决雪崩和击穿)
┌─────────────────────────────────────────────────────────────┐
│ 多级缓存架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ L1(本地缓存):进程内存,0 延迟 │ │
│ │ Caffeine / Guava Cache / dict,TTL = 5-10 秒 │ │
│ │ ↓ 未命中 │ │
│ │ L2(Redis):网络调用,0.5-1ms 延迟 │ │
│ │ 过期时间随机 24-25 小时 │ │
│ │ ↓ 未命中 │ │
│ │ L3(MySQL):磁盘 IO,5-10ms 延迟 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 即使 Redis 全过期,本地缓存 L1 还能撑 5-10 秒 │
│ → 在这期间缓存被陆续重建 → 数据库不会被打垮 │
│ │
│ 缺点:本地缓存占用进程内存,多实例部署时数据不一致 │
│ → 热点数据允许短暂不一致的场景才用 L1 │
│ │
└─────────────────────────────────────────────────────────────┘python
class MultiLevelCache:
def __init__(self):
self.l1_cache = {} # 本地内存缓存(可用 dict 或 Caffeine)
self.l1_ttl = 5 # L1: 5 秒过期
self.l2_ttl = 1800 # L2: 30 分钟(随机 24-25h)
def get(self, key):
# L1
if key in self.l1_cache:
entry = self.l1_cache[key]
if time.time() - entry["ts"] < self.l1_ttl:
return entry["val"]
# L2
val = r.get(key)
if val:
data = json.loads(val)
self.l1_cache[key] = {"val": data, "ts": time.time()}
return data
# L3
data = db.query(key)
if data:
expire = self.l2_ttl + random.randint(0, 3600)
r.setex(key, expire, json.dumps(data))
return data解法三:Redis 高可用(缓解,不能根治)
┌─────────────────────────────────────────────────────────────┐
│ Redis 高可用对雪崩的影响 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Sentinel / Cluster: │
│ → 节点 down 时快速切换,不中断服务 │
│ → 但解决不了"同时 miss"的问题 │
│ │
│ 真正的问题不在 Redis 是否高可用, │
│ 而在于"大量 key 同时过期导致同时 miss"。 │
│ │
│ 高可用只能加快恢复速度,不能防止雪崩发生。 │
│ 真正防雪崩还是要靠:过期时间随机化 + 多级缓存。 │
│ │
└─────────────────────────────────────────────────────────────┘第4节:穿透/击穿/雪崩串联对比
┌─────────────────────────────────────────────────────────────┐
│ 三个问题的本质区别和联串理解 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 穿透:数据根本不存在 │
│ → 查缓存(miss)→ 查数据库(也没有)→ 不缓存空值 │
│ → 解决方案:参数校验 → 布隆过滤器 → 缓存空值 │
│ → 关键词:恶意刷 ID / 数据确实不存在 │
│ │
│ ────────────────────────────────────────────────────────── │
│ │
│ 击穿:热点 key 过期的瞬间大量请求同时涌入 │
│ → 查缓存(miss)→ 查数据库(同时 10 万次) │
│ → 解决方案:互斥锁 → 逻辑过期 → 热点永不过期 │
│ → 关键词:同一个 key 过期 / 热点数据 │
│ │
│ ────────────────────────────────────────────────────────── │
│ │
│ 雪崩:大量 key 同时过期 │
│ → 查缓存(大量 miss)→ 查数据库(同时爆炸) │
│ → 解决方案:过期时间随机化 → 多级缓存 → 熔断降级 │
│ → 关键词:同时过期 / 大量不同 key │
│ │
│ ────────────────────────────────────────────────────────── │
│ │
│ 共同点:大量请求穿透到数据库,导致数据库崩溃 │
│ 区别:触发原因不同 → 解决方案不同 │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 场景 │ 特点 │ 核心解法 │ │
│ │ 穿透 │ 恶意/不存在 key │ 布隆过滤器 │ │
│ │ 击穿 │ 单热点 key 过期 │ 互斥锁/逻辑过期 │ │
│ │ 雪崩 │ 大量 key 同时过期│ 随机过期/多级缓存 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘第5节:缓存一致性——库存变了,缓存怎么处理
场景:用户看到的库存和实际库存不一致了
┌─────────────────────────────────────────────────────────────┐
│ │
│ 运营发现:商品 A 显示库存 100,但实际只有 10 了。 │
│ │
│ 排查过程: │
│ 管理员手动调整了库存:MySQL: 100 → 10 │
│ 但 Redis 缓存还是:100 │
│ │
│ 期间有 90 个用户下单成功,但仓库实际只有 10 件货。 │
│ │
│ 用户投诉:我们买到了,但你们说没货了。 │
│ │
└─────────────────────────────────────────────────────────────┘Cache-Aside(旁路缓存,最常用)
┌─────────────────────────────────────────────────────────────┐
│ Cache-Aside 读写流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 读操作: │
│ 1. 查缓存:cache = redis.get(key) │
│ → 命中 → 返回 ✅ │
│ │
│ 2. 缓存 miss → 查数据库:db = mysql.query(key) │
│ │
│ 3. 写入缓存:redis.setex(key, ttl, db) │
│ │
│ 写操作: │
│ 1. 写数据库:mysql.update(key, value) │
│ │
│ 2. 删除缓存(注意是删除,不是更新!): │
│ redis.del(key) │
│ │
│ 为什么写操作是"删除缓存"而不是"更新缓存"? │
│ 如果更新缓存时失败了 → 缓存和数据库不一致 │
│ 如果删除缓存时失败了 → 下次读会从数据库加载最新数据 │
│ │
│ 适用场景:读多写少的业务(90% 互联网场景) │
│ │
└─────────────────────────────────────────────────────────────┘延时双删:解决并发下的缓存不一致
┌─────────────────────────────────────────────────────────────┐
│ 延时双删的原理 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 问题:先更新数据库,再删缓存,中间有窗口期。 │
│ │
│ T1: DELETE cache ← 删旧缓存 │
│ T2: UPDATE mysql ← 更新数据库 │
│ T3: 线程 A 读请求:cache miss → 读 MySQL(旧值)→ 写旧缓存│
│ T4: DELETE cache ← 再删一次(删掉线程 A 的旧缓存) │
│ T5: 线程 B 读请求:cache miss → 读 MySQL(新值)→ 写新缓存│
│ │
│ 结果:缓存里是最新的值 ✅ │
│ │
│ sleep 时间怎么定? │
│ 经验值:0.5-1 秒(读请求的平均耗时) │
│ 更精确:监控读请求的 P99 耗时,设为 2 倍 │
│ │
└─────────────────────────────────────────────────────────────┘python
import time
def update_stock(product_id, new_stock):
# 1. 先删缓存
r.delete(f"product:{product_id}")
# 2. 更新数据库
db.execute("UPDATE products SET stock = %s WHERE id = %s",
new_stock, product_id)
# 3. 延时再删一次(关键!)
time.sleep(0.5) # 等 500ms,让并发读请求完成
r.delete(f"product:{product_id}")四种缓存模式对比
┌─────────────────────────────────────────────────────────────┐
│ 四种缓存读写模式对比 │
├─────────────────────────────────────────────────────────────┤
│ │
│ │ 模式 │ 读一致性 │ 写一致性 │ 复杂度 │ 适用场景 │
│ ├────────────────┼──────────┼──────────┼────────┼──────────┤
│ │ Cache-Aside │ 较高 │ 中 │ 低 │ 读多写少 │
│ ├────────────────┼──────────┼──────────┼────────┼──────────┤
│ │ Read-Through │ 高 │ 高 │ 中 │ 读多写少 │
│ ├────────────────┼──────────┼──────────┼────────┼──────────┤
│ │ Write-Through │ 高 │ 高 │ 高 │ 写多读少 │
│ ├────────────────┼──────────┼──────────┼────────┼──────────┤
│ │ Write-Behind │ 低 │ 低 │ 高 │ 极高写入 │
│ │
│ 大部分互联网场景:Cache-Aside(旁路缓存) │
│ │
└─────────────────────────────────────────────────────────────┘一致性的四个层次
┌─────────────────────────────────────────────────────────────┐
│ 缓存一致性的四个层次 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 强一致性(CP): │
│ → 不用缓存,所有请求直接查数据库 │
│ → 性能差,但数据一定正确 │
│ │
│ 最终一致性(AP): │
│ → Cache-Aside + 延时双删 │
│ → 有短暂不一致窗口(通常 <1 秒),但性能好 │
│ │
│ 弱一致性: │
│ → 本地缓存 + 短 TTL │
│ → 不一致窗口可能更长,但用户体验好 │
│ │
│ 业务兜底: │
│ → 库存超卖:下单时再检查一次库存 │
│ → 最终以数据库为准 │
│ │
│ 一句话总结: │
│ 强一致性代价高,最终一致性性价比好。 │
│ 关键数据的兜底检查永远不能省。 │
│ │
└─────────────────────────────────────────────────────────────┘升华:缓存的核心 trade-off
┌─────────────────────────────────────────────────────────────┐
│ 加入缓存后,系统获得了什么,接受了什么 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 获得了: │
│ ✅ 读取性能:10ms → 0.1ms,快 100 倍 │
│ ✅ 数据库保护:99% 的读请求被 Redis 拦截 │
│ ✅ 可扩展性:加机器比升级数据库便宜 │
│ │
│ 接受的 trade-off: │
│ ⚠️ 数据一致性:缓存和数据库可能有短暂不一致 │
│ ⚠️ 复杂度提升:穿透/击穿/雪崩/一致性都要处理 │
│ ⚠️ 运维成本:多了一个需要监控和调优的组件 │
│ │
│ 没有银弹: │
│ 性能和安全之间永远有 trade-off。 │
│ 互联网 99% 的场景选择 Redis 换性能,接受最终一致性。 │
│ 只有银行、证券等强一致性场景才不用缓存。 │
│ │
└─────────────────────────────────────────────────────────────┘"AI 可查 vs 必须理解"清单
AI 可查:
✅ 布隆过滤器的参数计算公式(n, p → m, k)
✅ 各语言的 Redis 布隆过滤器插件用法
✅ 多级缓存的具体实现(Caffeine / Guava / Python dict)
✅ 延时双删的 sleep 时间精确计算方法
必须理解:
🔴 穿透/击穿/雪崩三个问题的触发条件区别
🔴 穿透的核心解法:布隆过滤器(说"不存在"就一定不存在)
🔴 击穿的核心解法:互斥锁 vs 逻辑过期的适用场景
🔴 雪崩的核心解法:过期时间随机化 + 多级缓存
🔴 Cache-Aside 为什么写操作是"删除"而不是"更新"缓存
🔴 延时双删的 sleep 时间怎么定(以读请求 P99 为参考)学习状态:🟡 开始学习