高并发系统中的缓存设计

缓存策略:多级缓存(本地+Redis)、缓存穿透/击穿/雪崩解决方案、缓存一致性

多级缓存(本地 + Redis)

为什么需要多级缓存?

单一缓存(如只用 Redis)在超高并发场景下可能成为瓶颈(网络 IO、序列化开销、Redis 单点压力),多级缓存通过“就近取数据”来提升性能、降低延迟、减轻后端压力。


多级缓存结构(典型架构)

1
2
3
4
5
6
7
客户端请求
[本地缓存] —— 如:内存中的 LRU 缓存(进程内)
    ↓(未命中)
[分布式缓存] —— 如:Redis 集群
    ↓(未命中)
[数据库] —— MySQL / PostgreSQL

注意:本地缓存是“进程级”的,不同服务实例之间的本地缓存不共享。


本地缓存常用方案

  • Python 中functools.lru_cachecachetools(支持 TTL、最大容量)、diskcache(可持久化到磁盘)
  • Java 中:Caffeine、Guava Cache
  • 特点:访问速度极快(微秒级),但容量有限、数据不一致风险高

分布式缓存(Redis)

  • 支持集群、持久化、丰富数据结构
  • 适合共享数据、大容量缓存
  • 缺点:网络开销(毫秒级)、序列化成本、单点/集群运维复杂度

多级缓存更新策略(关键!)

方案 1:穿透式写(Write-Through)

  • 数据更新时,同时更新本地缓存 + Redis + 数据库
  • 优点:一致性较好
  • 缺点:写性能差,失败处理复杂

方案 2:回写式(Write-Back / Write-Behind)

  • 只写本地缓存,异步批量刷到 Redis/DB
  • 优点:写性能高
  • 缺点:宕机丢数据,一致性差

方案 3:失效策略(Cache-Aside) ← 最常用

  • 读:先读本地 → 未命中 → 读 Redis → 未命中 → 读 DB → 回填本地+Redis
  • 写:先更新 DB → 再删除本地缓存 + 删除 Redis 缓存
  • 优点:简单、通用、写性能好
  • 缺点:短暂不一致(最终一致),需处理并发写竞争

建议:Cache-Aside + 异步延迟双删 + 监听 Binlog 补偿,是互联网公司主流方案


缓存三大经典问题及解决方案


缓存穿透(Cache Penetration)

定义:

查询一个根本不存在的数据(如 ID=-1 或 不存在的用户),每次 DB 都查不到,缓存也不存储 → 所有请求打到 DB,压垮数据库。

解决方案:

方案 1:缓存空值(Null Caching)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 伪代码
value = redis.get(key)
if value is None:
    db_value = db.query(key)
    if db_value is None:
        # 缓存空值,设置较短TTL(如60s),防止恶意攻击
        redis.setex(key, "NULL", 60)
        return None
    else:
        redis.setex(key, db_value, 3600)
        return db_value
else:
    return value if value != "NULL" else None

方案 2:布隆过滤器(Bloom Filter)

  • 在缓存层前加一个布隆过滤器,快速判断“某个 key 是否可能存在”
  • 如果布隆过滤器说“不存在”,直接返回,不查缓存和 DB
  • 优点:内存占用小,查询极快
  • 缺点:有误判率(可能把存在的说成不存在 → 可接受),不支持删除

方案 3:接口参数校验 + 黑名单

  • 对明显非法请求(如负 ID、超长字符串)直接拦截
  • 记录恶意 IP 或参数,加入黑名单

缓存击穿(Cache Breakdown)

定义:

某个热点 key 过期瞬间,大量并发请求同时发现缓存失效,全部打到数据库 → 数据库瞬时压力暴增。

解决方案:

方案 1:热点数据永不过期(逻辑过期)

  • 不设 TTL,但在 Value 中加入“逻辑过期时间”
  • 后台异步线程定期刷新缓存
  • 请求时若发现逻辑过期,则触发异步更新,当前请求仍返回旧值

方案 2:互斥锁重建缓存(Mutex Lock)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# 伪代码(Redis + SETNX 实现)
value = redis.get(key)
if value is None:
    # 尝试获取锁
    lock_key = "lock:" + key
    if redis.setnx(lock_key, "1", ex=5):  # 加锁5秒
        try:
            # 双重检查(防止多个线程都进来)
            value = redis.get(key)
            if value is None:
                value = db.query(key)
                redis.setex(key, value, 3600)
        finally:
            redis.delete(lock_key)  # 释放锁
    else:
        # 等待或短暂休眠后重试
        time.sleep(0.1)
        return get_from_cache(key)  # 递归或循环重试
return value

方案 3:随机过期时间(防集体失效)

  • 对同类热点数据,设置 TTL 时加一个随机值(如 3600 ± 300 秒)
  • 避免大量 key 同时过期

缓存雪崩(Cache Avalanche)

定义:

大量缓存 key 在同一时间过期(如 Redis 宕机重启、或批量设置相同 TTL),导致所有请求穿透到 DB → 数据库被打挂。

和“击穿”区别:击穿是单个热点 key 失效,雪崩是“大面积集体失效”

解决方案:

方案 1:过期时间随机化(同击穿方案 3)

1
2
ttl = base_ttl + random.randint(-300, +300)
redis.setex(key, value, ttl)

方案 2:搭建高可用 Redis 集群

  • 主从 + 哨兵 / Redis Cluster,避免单点故障
  • 持久化(RDB+AOF)保障重启后快速恢复

方案 3:服务熔断 + 降级

  • 当检测到 DB 压力过大,自动降级:返回默认值、错误页、排队等待
  • 使用 Hystrix / Sentinel 实现熔断

方案 4:多级缓存 + 本地缓存兜底

  • 即使 Redis 全挂,本地缓存还能撑一段时间(虽然数据可能旧)

方案 5:缓存预热(Warm Up)

  • 系统启动或大促前,提前加载热点数据到缓存
  • 可通过脚本或定时任务触发

缓存一致性(Cache Consistency)

这是最难的部分 —— 如何保证“缓存中的数据”和“数据库中的数据”在各种并发场景下保持一致?


常见场景

  • 用户修改了数据 → 如何让缓存失效?
  • 多服务实例并发修改 → 如何避免脏数据?
  • 主从 DB 同步延迟 → 缓存读到旧数据怎么办?

解决方案与最佳实践

Cache-Aside + 延迟双删(推荐)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 更新数据时
db.update(key, new_value)

# 第一次删缓存
redis.delete(key)

# 等待一段时间(如500ms),让可能的“旧读请求”执行完
time.sleep(0.5)

# 第二次删缓存(兜底)
redis.delete(key)

💡 为什么延迟?防止“读请求在你更新 DB 前读了旧缓存,又在你删缓存后把旧值回填”

监听数据库 Binlog(终极方案)

  • 使用 Canal / Debezium 监听 MySQL Binlog
  • 一旦数据变更,异步发送消息(如 Kafka)通知缓存服务删除对应 key
  • 优点:解耦、可靠、最终一致
  • 缺点:架构复杂,有延迟

设置较短 TTL + 最终一致

  • 不追求强一致,允许短暂不一致(如 5 秒内)
  • 适用于对一致性要求不高的场景(如商品浏览量、文章点赞数)

读写锁 or 分布式锁(慎用)

  • 写操作前加锁,阻塞所有读(性能差,不推荐高并发场景)

版本号 / 时间戳比对

  • 缓存 Value 中带版本号,读取时与 DB 比对,不一致则刷新
  • 适合对一致性要求极高的场景(如金融)

总结对比表

问题类型原因解决方案关键词一致性级别
缓存穿透查询不存在的数据布隆过滤器、空值缓存、参数校验-
缓存击穿热点 key 过期,大量并发重建互斥锁、逻辑过期、随机 TTL强一致(锁)
缓存雪崩大量 key 同时失效或 Redis 宕机随机 TTL、集群高可用、熔断降级最终一致
缓存一致性DB 与缓存数据不一致延迟双删、监听 Binlog、短 TTL最终一致(推荐)

参考资料

  • 《Redis 设计与实现》 — 黄健宏
  • 《数据密集型应用系统设计》(DDIA)Chapter 5 & 7
  • 美团技术博客《缓存穿透、击穿、雪崩区别和解决方案》
  • 阿里云 Redis 最佳实践文档
版权声明:本文为原创,依据 CC BY-NC-SA 4.0 许可证进行授权,转载请附上出处链接及本声明。