Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

Sidebar Navigation

← Web 开发 / Web Development

中间件 / Middleware

1. Web 中间件全景 / The Web Middleware Landscape

2. 中间件——流量洪峰下的系统保护 / Middleware for Protecting Systems Under Traffic Spikes

cache layer

1. 缓存架构全景 / Cache Architecture Overview

2. Redis 深入——为什么你的缓存总是出问题 / Redis Internals and Cache Failure Modes

3. Redis 数据结构场景应用——什么时候用什么 / Choosing Redis Data Structures for Real Applications

4. 缓存策略——库存变了,缓存怎么处理 / Cache Strategies and Inventory Consistency

5. Redis 缓存三剑客——穿透/击穿/雪崩 + 一致性策略 / Redis Cache Penetration, Breakdown, Avalanche, and Consistency

6. Redis 分布式锁——从 SETNX 到 Redisson / Redis Distributed Locks from SETNX to Redisson

7. Redis Cluster 与 Sentinel 高可用架构 / Redis Cluster and Sentinel High-Availability Architecture

8. Redis 高级特性——Stream / PubSub / Module / LLM 应用

9. Redis 架构深度分析——为什么 Redis 能这么快 / Redis Architecture and the Sources of Its Performance

10. Redis 全景——为什么你的系统需要一个缓存层 / The Redis Landscape and Why Systems Need a Cache Layer

message queue

1. 消息队列全景——为什么你的系统需要一个中间人 / Message Queue Overview and Why Your System Needs a Middleman

2. Kafka 核心——为什么你的消息总是"丢"了 / Kafka Fundamentals and Message Delivery Semantics

3. 消息队列高级——死信队列、延迟消息、消息积压 / Advanced Messaging with Dead Letters, Delays, and Backlogs

4. Kafka Streams 与 Connect —— 让数据自己流动起来 / Kafka Streams and Connect for Streaming Data Pipelines

5. RabbitMQ 深度解析 —— 灵活路由与消息可靠性 / RabbitMQ Deep Dive into Flexible Routing and Reliability

6. 消息队列对比——为什么最终选了 Kafka / Comparing Message Queues and Choosing Kafka

search engine

1. 搜索引擎知识体系 / Search Engine Knowledge System

2. Elasticsearch——为什么 Like 查询总是那么慢 / Elasticsearch for Full-Text Search at Scale

3. Elasticsearch 查询 DSL 深入——为什么你的搜索总是不准 / Elasticsearch Query DSL Deep Dive

4. Elasticsearch 集群规划与运维——为什么你的集群总是"黄" / Elasticsearch Cluster Planning and Operations

5. Meilisearch 与轻量搜索替代方案——当 ES 太重时 / Meilisearch and Lightweight Search Alternatives

infrastructure

1. 基础设施组件全景 / Infrastructure Components Landscape

2. Nginx 与反向代理 / Nginx and Reverse Proxy

3. 服务发现 / Service Discovery

4. 配置中心 / Configuration Center

本页目录

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

第1节:缓存穿透——查询一个根本不存在的数据 ​

问题抽象 ​

┌─────────────────────────────────────────────────────────────┐
│                 缓存穿透的图书馆比喻                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  正常情况:                                                │
│  读者要书 → 先看展示柜(缓存)→ 有就拿走(命中)          │
│           → 没有就进图书馆找(未命中)→ 找到后放一本到展示柜│
│                                                             │
│  恶意情况(穿透):                                        │
│  有人拿着一张根本不存在的书单来要书                         │
│  → 展示柜没有 → 进图书馆找 → 图书馆也没有                │
│  → 不放入展示柜(因为没有这本书)                         │
│  → 下次同样的请求再来 → 再次进图书馆找 → 死循环         │
│                                                             │
│  核心问题:                                               │
│  不存在的数据,缓存层不存储 → 每次请求都穿透到数据库      │
│  恶意刷大量不存在的 ID → 数据库被打垮                     │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

推演:穿透的请求链路 ​

┌─────────────────────────────────────────────────────────────┐
│                 穿透的请求链路                               │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  第一次请求 id=9999999:                                   │
│  用户请求 → 查 Redis(miss)→ 查 MySQL(无数据)→ 返回空 │
│  → Redis 不缓存空值                                        │
│                                                             │
│  第二次请求 id=9999999:                                   │
│  用户请求 → 查 Redis(miss)→ 查 MySQL(无数据)→ 返回空 │
│                    ↑                                        │
│            每次都走数据库!                                 │
│                                                             │
│  10万个恶意ID × 每次都走数据库 = 10万次数据库查询         │
│  → 数据库爆炸 → 系统崩溃                                   │
│                                                             │
│  正常业务也可能穿透:                                      │
│  • 商品下架后爬虫仍在请求该商品 ID                        │
│  • 用户输入了不存在的搜索词                                │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

解法一:布隆过滤器(最优雅,推荐) ​

┌─────────────────────────────────────────────────────────────┐
│                 布隆过滤器原理                                │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  布隆过滤器 = 一个位数组 + 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    │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

解法二:缓存空值(最简单) ​

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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

适用场景:数据"确实不存在"的时间窗口已知且可控(如商品下架)。

解法三:参数校验(最根本) ​

python
# 网关层/业务层:最基础的防线
def check_request(product_id):
    if product_id < 0:          return False  # 负数 ID 不存在
    if product_id > 10_000_000: return False  # 超出商品 ID 范围
    return True
1
2
3
4
5

三层防御体系 ​

┌─────────────────────────────────────────────────────────────┐
│                 缓存穿透三层防御体系                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  第一层:参数校验(最便宜)                                 │
│  → 拦掉明显无效的请求(负数、超范围)                     │
│                                                             │
│  第二层:布隆过滤器(推荐)                                 │
│  → 在 Redis 层面判断数据是否可能存在                       │
│  → 不存在的请求直接返回,不碰数据库                       │
│                                                             │
│  第三层:缓存空值(兜底)                                  │
│  → 即使布隆过滤器误判(说存在但实际不存在)               │
│  → 数据库查不到也缓存起来,下次直接命中                    │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐ │
│  │  请求进来                                            │ │
│  │    ↓                                                 │ │
│  │  参数校验:合法?                                    │ │
│  │    ↓ 不合法 → 直接返回空                             │ │
│  │  布隆过滤器:可能存在?                              │ │
│  │    ↓ 不存在 → 直接返回空                             │ │
│  │  Redis 缓存:命中?                                 │ │
│  │    ↓ 未命中 → 查 DB → 缓存结果                      │ │
│  └─────────────────────────────────────────────────────┘ │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

第2节:缓存击穿——热点数据过期那一瞬间 ​

问题抽象 ​

┌─────────────────────────────────────────────────────────────┐
│                 缓存击穿的图书馆比喻                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  一本超级热门的书(热点数据),放在展示柜里只有一本。       │
│  所有人都来借这本书。                                       │
│                                                             │
│  问题来了:这本书的"展示时间"(缓存过期)到了,           │
│  要拿回图书馆重新放一本书进去。                             │
│                                                             │
│  就在这个交接的瞬间:                                      │
│  所有读者同时冲向图书馆 → 图书馆被挤爆了。                │
│                                                             │
│  核心问题:                                               │
│  大量请求集中在同一个 key 过期的那一刻,                   │
│  同时穿透到数据库 → 数据库被击穿。                         │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

真实场景:秒杀商品缓存过期 ​

┌─────────────────────────────────────────────────────────────┐
│                 缓存击穿的真实场景                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  凌晨 0 点,秒杀商品 A 的缓存刚刚过期。                    │
│                                                             │
│  10:00:00 - 100 个请求同时到达:                          │
│  请求 1:查 Redis(过期了)→ 查 MySQL → 查到了           │
│  请求 2:查 Redis(过期了)→ 查 MySQL → 查到了           │
│  请求 3:查 Redis(过期了)→ 查 MySQL → 查到了           │
│  ...(100 个请求全部打到了数据库)                         │
│                                                             │
│  如果商品是爆款,可能 10 万个请求同时进来                  │
│  → 10 万次数据库查询 → 数据库崩溃                          │
│                                                             │
│  穿透 vs 击穿的区别:                                      │
│  • 穿透:大量不同的 key 都不存在                          │
│  • 击穿:同一个热 key 过期的瞬间大量请求同时涌入           │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

解法一:互斥锁(保守但稳妥) ​

┌─────────────────────────────────────────────────────────────┐
│                 互斥锁原理                                   │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  第一个请求拿到锁 → 查数据库 → 写入缓存 → 释放锁          │
│  其他请求等待锁 → 锁被释放 → 查 Redis(有了!)→ 直接命中│
│                                                             │
│  效果:只有 1 个请求查数据库,其他全部等锁后命中缓存       │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
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)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

解法二:逻辑过期(推荐,热点数据) ​

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))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

为什么推荐逻辑过期:

┌─────────────────────────────────────────────────────────────┐
│                 互斥锁 vs 逻辑过期 对比                       │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  │ 维度          │ 互斥锁            │ 逻辑过期              │
│  ├───────────────┼──────────────────┼──────────────────────┤
│  │ 原理          │ 只有一个线程查 DB │ 所有线程都能返回旧数据│
│  ├───────────────┼──────────────────┼──────────────────────┤
│  │ 响应时间      │ 等锁的线程慢      │ 所有线程都快速返回    │
│  ├───────────────┼──────────────────┼──────────────────────┤
│  │ 数据一致性    │ 一定是最新的      │ 短暂不一致(~100ms) │
│  ├───────────────┼──────────────────┼──────────────────────┤
│  │ 实现复杂度    │ 中                │ 低                    │
│  ├───────────────┼──────────────────┼──────────────────────┤
│  │ 适用场景      │ 数据一致性要求高  │ 热点数据,允许短暂不一致│
│                                                             │
│  推荐:热点数据 → 逻辑过期;强一致数据 → 互斥锁            │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

解法三:热点数据永不过期(极端热点) ​

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)
1
2
3
4
5
6
7
8
9
10
11
12
13
14

适用场景:双十一爆款等极端热点场景。代价是数据更新有延迟。


第3节:缓存雪崩——所有缓存同时失效 ​

问题抽象 ​

┌─────────────────────────────────────────────────────────────┐
│                 缓存雪崩的图书馆比喻                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  展示柜里所有书的"展示时间"都是同一个时刻设置的。           │
│                                                             │
│  灾难场景:凌晨 0 点,你设的"每日缓存过期时间"全部到期。  │
│                                                             │
│  一瞬间,所有读者同时冲向图书馆——                          │
│  不是一本书,是所有书同时过期——                            │
│  图书馆被挤垮了。                                          │
│                                                             │
│  核心问题:                                               │
│  大量不同的 key 在同一时刻同时过期,                       │
│  所有请求同时穿透到数据库 → 数据库被打垮。                 │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

真实场景:统一过期时间 ​

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 点,所有缓存同时过期
# → 所有请求同时穿透到数据库
# → 数据库瞬间压力暴增
1
2
3
4
5
6
7
8
9
10

解法一:过期时间随机化 ​

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
1
2
3
4
5
6
7
8
9
10
11
12
13
14

解法二:多级缓存(同时解决雪崩和击穿) ​

┌─────────────────────────────────────────────────────────────┐
│                 多级缓存架构                                 │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────────────────────────────────────────────┐ │
│  │  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                  │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

解法三:Redis 高可用(缓解,不能根治) ​

┌─────────────────────────────────────────────────────────────┐
│                 Redis 高可用对雪崩的影响                      │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Sentinel / Cluster:                                       │
│  → 节点 down 时快速切换,不中断服务                        │
│  → 但解决不了"同时 miss"的问题                             │
│                                                             │
│  真正的问题不在 Redis 是否高可用,                          │
│  而在于"大量 key 同时过期导致同时 miss"。                  │
│                                                             │
│  高可用只能加快恢复速度,不能防止雪崩发生。                 │
│  真正防雪崩还是要靠:过期时间随机化 + 多级缓存。           │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

第4节:穿透/击穿/雪崩串联对比 ​

┌─────────────────────────────────────────────────────────────┐
│              三个问题的本质区别和联串理解                    │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  穿透:数据根本不存在                                       │
│  → 查缓存(miss)→ 查数据库(也没有)→ 不缓存空值          │
│  → 解决方案:参数校验 → 布隆过滤器 → 缓存空值              │
│  → 关键词:恶意刷 ID / 数据确实不存在                      │
│                                                             │
│  ────────────────────────────────────────────────────────── │
│                                                             │
│  击穿:热点 key 过期的瞬间大量请求同时涌入                  │
│  → 查缓存(miss)→ 查数据库(同时 10 万次)               │
│  → 解决方案:互斥锁 → 逻辑过期 → 热点永不过期             │
│  → 关键词:同一个 key 过期 / 热点数据                      │
│                                                             │
│  ────────────────────────────────────────────────────────── │
│                                                             │
│  雪崩:大量 key 同时过期                                    │
│  → 查缓存(大量 miss)→ 查数据库(同时爆炸)              │
│  → 解决方案:过期时间随机化 → 多级缓存 → 熔断降级         │
│  → 关键词:同时过期 / 大量不同 key                         │
│                                                             │
│  ────────────────────────────────────────────────────────── │
│                                                             │
│  共同点:大量请求穿透到数据库,导致数据库崩溃               │
│  区别:触发原因不同 → 解决方案不同                        │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐ │
│  │  场景         │ 特点             │ 核心解法             │ │
│  │  穿透         │ 恶意/不存在 key  │ 布隆过滤器           │ │
│  │  击穿         │ 单热点 key 过期  │ 互斥锁/逻辑过期     │ │
│  │  雪崩         │ 大量 key 同时过期│ 随机过期/多级缓存   │ │
│  └─────────────────────────────────────────────────────┘ │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

第5节:缓存一致性——库存变了,缓存怎么处理 ​

场景:用户看到的库存和实际库存不一致了 ​

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  运营发现:商品 A 显示库存 100,但实际只有 10 了。        │
│                                                             │
│  排查过程:                                              │
│  管理员手动调整了库存:MySQL: 100 → 10                   │
│  但 Redis 缓存还是:100                                   │
│                                                             │
│  期间有 90 个用户下单成功,但仓库实际只有 10 件货。      │
│                                                             │
│  用户投诉:我们买到了,但你们说没货了。                   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13

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% 互联网场景)             │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

延时双删:解决并发下的缓存不一致 ​

┌─────────────────────────────────────────────────────────────┐
│                 延时双删的原理                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  问题:先更新数据库,再删缓存,中间有窗口期。              │
│                                                             │
│  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 倍               │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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}")
1
2
3
4
5
6
7
8
9
10
11
12
13

四种缓存模式对比 ​

┌─────────────────────────────────────────────────────────────┐
│              四种缓存读写模式对比                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  │ 模式           │ 读一致性 │ 写一致性 │ 复杂度 │ 适用场景  │
│  ├────────────────┼──────────┼──────────┼────────┼──────────┤
│  │ Cache-Aside   │  较高   │   中    │   低   │ 读多写少  │
│  ├────────────────┼──────────┼──────────┼────────┼──────────┤
│  │ Read-Through  │  高     │   高    │   中   │ 读多写少  │
│  ├────────────────┼──────────┼──────────┼────────┼──────────┤
│  │ Write-Through │  高     │   高    │   高   │ 写多读少  │
│  ├────────────────┼──────────┼──────────┼────────┼──────────┤
│  │ Write-Behind  │  低     │   低    │   高   │ 极高写入  │
│                                                             │
│  大部分互联网场景:Cache-Aside(旁路缓存)                 │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

一致性的四个层次 ​

┌─────────────────────────────────────────────────────────────┐
│              缓存一致性的四个层次                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  强一致性(CP):                                        │
│  → 不用缓存,所有请求直接查数据库                         │
│  → 性能差,但数据一定正确                                  │
│                                                             │
│  最终一致性(AP):                                      │
│  → Cache-Aside + 延时双删                               │
│  → 有短暂不一致窗口(通常 <1 秒),但性能好              │
│                                                             │
│  弱一致性:                                              │
│  → 本地缓存 + 短 TTL                                     │
│  → 不一致窗口可能更长,但用户体验好                       │
│                                                             │
│  业务兜底:                                              │
│  → 库存超卖:下单时再检查一次库存                       │
│  → 最终以数据库为准                                        │
│                                                             │
│  一句话总结:                                              │
│  强一致性代价高,最终一致性性价比好。                    │
│  关键数据的兜底检查永远不能省。                          │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

升华:缓存的核心 trade-off ​

┌─────────────────────────────────────────────────────────────┐
│              加入缓存后,系统获得了什么,接受了什么            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  获得了:                                                   │
│  ✅ 读取性能:10ms → 0.1ms,快 100 倍                     │
│  ✅ 数据库保护:99% 的读请求被 Redis 拦截                  │
│  ✅ 可扩展性:加机器比升级数据库便宜                       │
│                                                             │
│  接受的 trade-off:                                        │
│  ⚠️  数据一致性:缓存和数据库可能有短暂不一致               │
│  ⚠️  复杂度提升:穿透/击穿/雪崩/一致性都要处理             │
│  ⚠️  运维成本:多了一个需要监控和调优的组件               │
│                                                             │
│  没有银弹:                                               │
│  性能和安全之间永远有 trade-off。                         │
│  互联网 99% 的场景选择 Redis 换性能,接受最终一致性。      │
│  只有银行、证券等强一致性场景才不用缓存。                 │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

"AI 可查 vs 必须理解"清单 ​

AI 可查:
✅ 布隆过滤器的参数计算公式(n, p → m, k)
✅ 各语言的 Redis 布隆过滤器插件用法
✅ 多级缓存的具体实现(Caffeine / Guava / Python dict)
✅ 延时双删的 sleep 时间精确计算方法

必须理解:
🔴 穿透/击穿/雪崩三个问题的触发条件区别
🔴 穿透的核心解法:布隆过滤器(说"不存在"就一定不存在)
🔴 击穿的核心解法:互斥锁 vs 逻辑过期的适用场景
🔴 雪崩的核心解法:过期时间随机化 + 多级缓存
🔴 Cache-Aside 为什么写操作是"删除"而不是"更新"缓存
🔴 延时双删的 sleep 时间怎么定(以读请求 P99 为参考)
1
2
3
4
5
6
7
8
9
10
11
12
13

学习状态:🟡 开始学习

最后更新于:

Pager
上一篇4. 缓存策略——库存变了,缓存怎么处理 / Cache Strategies and Inventory Consistency
下一篇6. Redis 分布式锁——从 SETNX 到 Redisson / Redis Distributed Locks from SETNX to Redisson

持续记录,持续成长

Copyright © Tidenflow