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

本页目录

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

📅 创建时间:2026-05-08 🏷️ 标签:#缓存策略 #Cache-Aside #Write-Through #延时双删 #多级缓存 #数据一致性 📚 前置知识:[[00-backend-overview]] [[01-redis-deep]](Redis 三问) [[09-mysql-optimization]](MySQL 优化) 📚 相关知识:[[/03-web/06-databases-and-data-access/05-redis]](Redis 基础)


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

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

这一章,我们理解缓存和数据库之间的数据一致性。


第1节:四种经典缓存模式 ​

模式一:Cache-Aside(旁路缓存,最常用) ​

┌─────────────────────────────────────────────────────────────┐
│                 Cache-Aside(旁路缓存)                      │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  读操作:                                              │
│  ┌─────────────────────────────────────────────────────┐ │
│  │  1. 查缓存                                          │ │
│  │     cache = redis.get(key)                          │ │
│  │     if cache: return cache  ✅ 命中,返回            │ │
│  │                                                     │ │
│  │  2. 缓存 miss,查数据库                             │ │
│  │     db = mysql.query(key)                           │ │
│  │                                                     │ │
│  │  3. 写入缓存                                        │ │
│  │     redis.setex(key, ttl, db)                      │ │
│  │     return 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
26
27
28
29
30
31
32
33
34

模式二:Read-Through(读穿透) ​

python
# Cache-Aside:应用自己负责读写缓存和数据库
# Read-Through:缓存层自动帮你加载

# 用户代码:只查缓存
cache.get(key)

# 缓存层自动完成:
# if cache miss: → 从数据库加载 → 存入缓存 → 返回
1
2
3
4
5
6
7
8

模式三:Write-Through(写穿透) ​

python
# 写操作:同时写缓存和数据库

def update(key, value):
    # 缓存和数据库同时更新
    mysql.update(key, value)
    redis.set(key, value)
    # 要么都成功,要么都失败(需要事务或补偿)
1
2
3
4
5
6
7

模式四:Write-Behind(写回) ​

python
# 写操作:只写缓存,异步写数据库

def update(key, value):
    # 立即写缓存
    redis.set(key, value)
    # 异步写数据库(通过消息队列)
    mq.send("update_db", {"key": key, "value": value})

# 后台消费者处理消息队列,定期同步到数据库
1
2
3
4
5
6
7
8
9

四种模式对比 ​

┌─────────────────────────────────────────────────────────────┐
│              四种缓存模式对比                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  │ 模式        │ 读一致性 │ 写一致性 │ 复杂度 │ 适用场景  │
│  ├─────────────┼──────────┼──────────┼────────┼──────────┤
│  │ 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

第2节:缓存一致性的核心问题——双写问题 ​

场景:缓存和数据库同时写,谁先谁后 ​

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  方案 A:先更新数据库,再更新缓存                        │
│                                                             │
│  T1: 更新 MySQL stock = 10                               │
│  T2: 更新 Redis stock = 10  ← 此时缓存是最新的          │
│  T3: 读请求:Redis = 10 ✅                               │
│                                                             │
│  问题:T1 成功后、T2 执行前,系统崩溃了                 │
│  → MySQL = 10,Redis = 旧值                             │
│                                                             │
│  方案 B:先更新缓存,再更新数据库                        │
│                                                             │
│  T1: 更新 Redis stock = 10                               │
│  T2: 读请求:Redis = 10 ✅                               │
│  T3: 更新 MySQL stock = 10  ← 此时缓存是最新的          │
│                                                             │
│  问题:T1 成功后、T3 执行前,系统崩溃了                 │
│  → MySQL = 旧值,Redis = 10                             │
│  → 下次读还是走缓存,数据不一致                         │
│                                                             │
│  结论:无论谁先谁后,都无法保证强一致性                 │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

解决方案:延时双删 ​

python
def update_stock(product_id, new_stock):
    # 1. 先删缓存
    redis.del(f"product:{product_id}")

    # 2. 更新数据库
    mysql.execute("UPDATE products SET stock = %s WHERE id = %s",
                 new_stock, product_id)

    # 3. 延时再删一次(关键!)
    # 等待一小段时间,让可能正在读取旧缓存的请求完成
    # 然后再删一次缓存
    import time
    time.sleep(0.5)  # 500ms,等待读请求完成
    redis.del(f"product:{product_id}")
1
2
3
4
5
6
7
8
9
10
11
12
13
14

为什么延时双删有效 ​

┌─────────────────────────────────────────────────────────────┐
│                 延时双删的原理                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  时间线:                                               │
│                                                             │
│  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

第3节:多级缓存——本地缓存 + Redis ​

问题:Redis 虽然快,但还是要走网络 ​

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  场景:热点数据(如商品详情)                            │
│                                                             │
│  L1(本地缓存):进程内存,0 延迟                       │
│  L2(Redis):网络调用,0.5-1ms 延迟                   │
│  L3(MySQL):磁盘 IO,5-10ms 延迟                    │
│                                                             │
│  优化:                                                   │
│  L1 命中 → 直接返回(不查 Redis)                      │
│  L1 未命中 → 查 Redis → 查 MySQL                      │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13

多级缓存实现 ​

python
import time
import threading
from functools import lru_cache

# Python 本地缓存:Caffeine(Java)/ Guava(Java)
# Python 可以用 dict + TTL 或 lru_cache

class MultiLevelCache:
    def __init__(self, redis_client, mysql_client):
        self.redis = redis_client
        self.db = mysql_client
        # L1:本地缓存(dict + 过期时间)
        self.l1_cache = {}
        self.l1_lock = threading.Lock()
        self.l1_ttl = 5  # 本地缓存 5 秒

    def get(self, product_id):
        key = f"product:{product_id}"

        # L1:本地缓存
        with self.l1_lock:
            if key in self.l1_cache:
                entry = self.l1_cache[key]
                if time.time() - entry["ts"] < self.l1_ttl:
                    return entry["data"]  # L1 命中
                del self.l1_cache[key]

        # L2:Redis
        val = self.redis.get(key)
        if val:
            data = json.loads(val)
            # 回填 L1
            with self.l1_lock:
                self.l1_cache[key] = {"data": data, "ts": time.time()}
            return data

        # L3:MySQL
        data = self.db.query("SELECT * FROM products WHERE id = %s", product_id)
        if data:
            self.redis.setex(key, 3600, json.dumps(data))
            with self.l1_lock:
                self.l1_cache[key] = {"data": data, "ts": time.time()}
        return data

    def invalidate(self, product_id):
        key = f"product:{product_id}"
        self.redis.del(key)
        with self.l1_lock:
            if key in self.l1_cache:
                del self.l1_cache[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
37
38
39
40
41
42
43
44
45
46
47
48
49
50

本地缓存的问题:数据更新后怎么通知 ​

┌─────────────────────────────────────────────────────────────┐
│              本地缓存的数据同步问题                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  问题:多实例部署时,实例 A 更新了数据                     │
│       实例 B 的本地缓存还是旧数据                         │
│                                                             │
│  解决:                                                   │
│  1. L1 缓存 TTL 设短(5-10 秒)                         │
│     → 旧数据最多存 10 秒                                  │
│                                                             │
│  2. 发布-订阅:数据变更时发布消息                         │
│     实例 A 更新数据 → 通知 Redis PubSub → 实例 B 清理本地缓存 │
│                                                             │
│  3. 主动查询:后台定期刷新                                │
│     定时任务对比 L1 和 Redis,不一致时清理                │
│                                                             │
│  最佳实践:                                              │
│  本地缓存只存"允许短暂不一致"的数据                     │
│  库存等强一致性数据:只用 Redis,不加 L1                │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

第4节:缓存预热——系统启动后缓存是空的 ​

场景:秒杀系统重启后,所有请求同时打数据库 ​

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  凌晨 3 点,系统发布重启。                                │
│                                                             │
│  凌晨 3:01,监控报警:MySQL CPU 100%                    │
│                                                             │
│  原因:Redis 缓存是空的,所有请求穿透到 MySQL           │
│       10000 QPS × MySQL = 10000 QPS → 爆炸              │
│                                                             │
│  解决:缓存预热——系统启动后提前加载数据到缓存        │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12

预热方案 ​

python
def warmup_cache():
    """系统启动时执行"""
    print("开始缓存预热...")

    # 1. 预热热点数据(TOP 1000 商品)
    hot_products = db.query("""
        SELECT * FROM products
        WHERE status = 'active'
        ORDER BY view_count DESC
        LIMIT 1000
    """)
    for p in hot_products:
        redis.setex(f"product:{p['id']}", 86400, json.dumps(p))

    # 2. 预热用户数据(最近活跃的用户)
    active_users = db.query("""
        SELECT * FROM users
        WHERE last_login > DATE_SUB(NOW(), INTERVAL 7 DAY)
        LIMIT 10000
    """)
    for u in active_users:
        redis.setex(f"user:{u['id']}", 3600, json.dumps(u))

    # 3. 预热配置数据
    configs = db.query("SELECT * FROM configs")
    for c in configs:
        redis.setex(f"config:{c['key']}", 86400, c['value'])

    print(f"预热完成:{len(hot_products)} 商品,{len(active_users)} 用户")
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

升华:缓存一致性的 trade-off ​

┌─────────────────────────────────────────────────────────────┐
│              缓存一致性的四个层次                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  强一致性(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

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

AI 可查:
✅ 各语言的多级缓存实现(Caffeine / Guava / Python dict)
✅ Redis PubSub 的具体 API
✅ 缓存预热脚本的具体写法

必须理解:
🔴 Cache-Aside 的读写流程(为什么读是"命中返回+miss查库+写回",写是"先库后删")
🔴 为什么不能"更新缓存"而要"删除缓存"
🔴 延时双删的 sleep 时间怎么定
🔴 多级缓存中 L1 本地缓存的数据同步问题
1
2
3
4
5
6
7
8
9
10

学习状态:🟡 开始学习

最后更新于:

Pager
上一篇3. Redis 数据结构场景应用——什么时候用什么 / Choosing Redis Data Structures for Real Applications
下一篇5. Redis 缓存三剑客——穿透/击穿/雪崩 + 一致性策略 / Redis Cache Penetration, Breakdown, Avalanche, and Consistency

持续记录,持续成长

Copyright © Tidenflow