返回首页
·6 min read·学习笔记

缓存策略学习笔记:从 Cache-Aside 到多级缓存

为什么需要缓存

在系统设计中,性能瓶颈往往出现在 I/O 上。数据库查询是毫秒级的,而内存访问是纳秒级的——两者相差 10^5 倍。缓存就是在两者之间架起一座桥梁。

常见缓存策略

1. Cache-Aside(旁路缓存)

最常用的缓存模式。应用程序先查缓存,不命中再查数据库,然后写入缓存。

读取:
1. 查缓存 → 命中则返回
2. 未命中 → 查数据库 → 写入缓存 → 返回

写入:
1. 更新数据库
2. 删除缓存(而非更新缓存)
def get_user(user_id):
    # 1. 查缓存
    user = cache.get(f"user:{user_id}")
    if user:
        return user
    
    # 2. 查数据库
    user = db.query("SELECT * FROM users WHERE id = ?", user_id)
    
    # 3. 写入缓存
    cache.set(f"user:{user_id}", user, ttl=3600)
    return user

为什么删除而不是更新缓存?

  • 避免并发写入导致的数据不一致
  • 懒加载:只在需要时才计算缓存值
  • 有些缓存值计算成本高,频繁更新浪费资源

2. Write-Through(穿透写入)

写入时同时更新缓存和数据库。

写入:
1. 写缓存
2. 写数据库

优点:缓存始终是最新的。 缺点:写入延迟增加。

3. Write-Behind(异步写入)

写入时只更新缓存,异步批量写入数据库。

写入:
1. 写缓存
2. 异步队列 → 批量写数据库

优点:写入性能极高。 缺点:缓存宕机可能丢数据,实现复杂。

缓存一致性

经典问题:先更新数据库还是先删缓存?

| 操作顺序 | 并发风险 | |---------|---------| | 先删缓存,后更新DB | 另一个线程读到旧数据并写入缓存 | | 先更新DB,后删缓存 | 删缓存失败导致缓存残留旧数据 |

推荐方案:延迟双删

def update_user(user_id, data):
    # 1. 先删缓存
    cache.delete(f"user:{user_id}")
    
    # 2. 更新数据库
    db.update("users", data, where={"id": user_id})
    
    # 3. 延迟再删一次(防止并发读写入旧值)
    schedule_delayed_task(
        lambda: cache.delete(f"user:{user_id}"),
        delay=0.5  # 500ms后执行
    )

缓存常见问题

1. 缓存穿透

查询一个不存在的数据,缓存永远不命中,请求直达数据库。

解决方案:

  • 缓存空值(设置短 TTL)
  • 布隆过滤器(快速判断数据是否存在)
def get_user(user_id):
    user = cache.get(f"user:{user_id}")
    if user is not None:
        return user if user != "NULL" else None
    
    user = db.query("SELECT * FROM users WHERE id = ?", user_id)
    if user:
        cache.set(f"user:{user_id}", user, ttl=3600)
    else:
        # 缓存空值,短TTL
        cache.set(f"user:{user_id}", "NULL", ttl=60)
    return user

2. 缓存击穿

热点数据过期瞬间,大量请求直达数据库。

解决方案:

  • 互斥锁(只让一个请求查数据库)
  • 热点数据不过期

3. 缓存雪崩

大量缓存同时过期,或缓存服务宕机。

解决方案:

  • TTL 加随机偏移
  • 多级缓存
  • 缓存高可用(Redis 集群)

多级缓存架构

请求 → 本地缓存 → 分布式缓存 → 数据库
       (Caffeine)   (Redis)      (MySQL)
# 多级缓存实现
def get_data(key):
    # L1: 本地缓存 (纳秒级)
    val = local_cache.get(key)
    if val is not None:
        return val
    
    # L2: 分布式缓存 (毫秒级)
    val = redis.get(key)
    if val is not None:
        local_cache.set(key, val, ttl=60)
        return val
    
    # L3: 数据库 (十毫秒级)
    val = db.query(key)
    if val:
        redis.set(key, val, ttl=3600)
        local_cache.set(key, val, ttl=60)
    return val

缓存淘汰策略

| 策略 | 说明 | 适用场景 | |------|------|---------| | LRU | 最近最少使用 | 通用场景 | | LFU | 最少使用频率 | 热点数据明显 | | FIFO | 先进先出 | 简单场景 | | TTL | 按过期时间 | 时效性数据 | | Random | 随机淘汰 | 无明显访问模式 |

学习总结

缓存设计是系统设计中性价比最高的一环。核心思路:

  1. 先用 Cache-Aside 模式 — 简单有效
  2. 注意一致性 — 延迟双删解决大部分问题
  3. 预防三大问题 — 穿透、击穿、雪崩
  4. 多级缓存兜底 — 本地缓存 + 分布式缓存

缓存不是银弹。每加一层缓存,就多一层复杂度。衡量收益和成本,才是好的架构决策。

小楼春雨

@站长