缓存如何设计?

访客 性能优化 2

从原理到落地的完整指南

📚 目录导读

  1. 缓存设计的核心原则
  2. 缓存失效策略对比
  3. 缓存穿透、雪崩与击穿的解决方案
  4. 分层缓存架构设计
  5. 缓存与数据库一致性方案
  6. 常见问答FAQ

缓存设计的核心原则

缓存设计的首要原则是“只缓存高频访问且更新不频繁的数据”,很多团队一开始会试图缓存所有数据,但这样不仅浪费内存,还会导致数据一致性问题,从搜索引擎聚合的实战案例来看,优秀的缓存设计通常遵循“二八原则”:20%的热点数据承担了80%的请求流量。

在设计之初需要回答三个关键问题:

  • 数据访问频率如何?(QPS/并发量)
  • 数据更新频率如何?(秒级/分钟级/小时级)
  • 数据一致性要求有多高?(强一致/最终一致)

例如电商系统的商品详情页,访问量极高但价格、库存变化快,需要设置较短的缓存时间(如1-5分钟);而用户头像、静态配置则适合长缓存(30分钟到1小时)。


缓存失效策略对比

根据Google搜索排名靠前的技术博客综合,常见的失效策略有:

策略名称 实现方式 适用场景 缺点
TTL(过期时间) 设置绝对过期时间 简单数据 同一时间大量失效
滑动窗口 每次访问重置TTL 用户会话 活跃流量下缓存长存
主动淘汰 LRU/LFU算法 内存受限系统 实现复杂
懒淘汰 读取时判断是否过期 低访问频次数据 存在脏读窗口

重点推荐组合策略:TTL + 懒淘汰,这是目前多数互联网大厂(如抖音、美团)采用的核心方案,当缓存过期时,不直接删除,而是标记为“即将过期”,下次读取时触发异步刷新,避免热点数据同时过期的“惊群效应”。


缓存穿透、雪崩与击穿的解决方案

1 缓存穿透(Cache Penetration)

问题:大量请求直接穿过缓存访问数据库,没有命中任何缓存数据。

解决方案(综合多篇SEO排名靠前文章):

  • 布隆过滤器:在缓存前加一层位图过滤,判断key是否存在,例如1000万个商品ID,用1GB内存即可实现99%的过滤率。
  • 空值缓存:将数据库查不到的结果也缓存(如设置Null,TTL短至30秒),防止恶意攻击持续穿透。
  • 参数校验:在入口层校验key格式,拒绝非法ID。

2 缓存雪崩(Cache Avalanche)

问题:大范围缓存同时失效,导致数据库压力暴涨。

实战方案

  • 缓存失效时间加随机值(基础TTL + 0-3分钟随机偏移)
  • 多级缓存降级:本地缓存(如Caffeine)+ 分布式缓存(如Redis)
  • 熔断限流机制:当数据库QPS超过阈值,直接返回默认值或降级结果

3 缓存击穿(Cache Breakdown)

问题:单个热点数据失效时,高并发请求直达数据库。

核心解法

  • 互斥锁(Mutex):只允许一个线程去查询数据库并更新缓存,其他线程等待重试。
  • 逻辑过期:不物理删除缓存,而是设置一个逻辑过期时间字段,后台异步更新,例如某大厂商品详情页的伪代码:
    if(cache.expireTime < System.currentTimeMillis()){
        // 只允许一个线程去刷新缓存
        asyncRefresh(cacheKey);
        return cache.oldData; // 返回旧数据保证可用
    }

分层缓存架构设计

基于实际项目经验(参考了GitHub上star过万的缓存设计仓库),推荐三级缓存架构

  1. 本地缓存(第1级):Caffeine或Guava Cache,放在应用进程内,响应时间<1ms,适合每个节点的独有数据,如用户会话。
  2. 分布式缓存(第2级):Redis Cluster,响应时间1-5ms,存储全局热点数据,如商品列表。
  3. 数据库(第3级):MySQL等持久层,只负责兜底。

数据流动逻辑: 请求 → 读取本地缓存(命中则返回)→ 读取Redis(命中则异步回填本地缓存)→ 读取数据库(回填Redis并发送MQ异步更新本地缓存)

这种架构在应对双11等峰值时,本地缓存可承受单机5万QPS,Redis集群分担80%的数据库压力。


缓存与数据库一致性方案

这是缓存设计中最难处理的问题,综合多家互联网公司技术博客,主流方案有:

1 延时双删(最常用)

写请求时:先删除Redis缓存 → 更新数据库 → 休眠500ms再删除缓存 读请求时:读Redis → 未命中则读数据库 → 写Redis 注意:休眠时间需要比主从同步延迟长,否则可能读到旧数据。

2 基于Binlog的异步更新(强一致方案)

使用Canal监听数据库binlog,当数据变更时解析消息并发送到MQ,消费端更新Redis。 优势:解耦,不侵入业务代码。劣势:引入额外中间件。

3 订阅模式(最终一致)

在数据库写入后,通过MQ通知所有缓存节点删除或更新key,例如阿里云DTS实时同步方案。

建议:对于大多数业务(如资讯、社区),采用“延时双删+读时强校验”即可满足要求;对于金融、库存等场景,优先使用Binlog方案。


常见问答FAQ

Q1: 缓存的数据量应该设计多大?

A: 通用经验是:缓存内存占用不超过总业务的20%,且热点数据命中率需保持在95%以上,通过Redis的INFO memoryINFO stats监控命中率,低于90%时考虑扩容。

Q2: 如何防止缓存热点导致Redis单节点过载?

A: 可采用本地缓存+Redis分片组合方案,对于单个热点key(如某爆款商品),将数据复制到多个分片,前端请求使用hash散列到不同分片,避免单点压力。

Q3: 缓存淘汰时如何保证不丢重要数据?

A: 使用“冷热分离”策略,热数据使用LRU淘汰,但通过定期扫描将所有访问的节点加入“白名单”,白名单内数据不参与淘汰,推荐参考Wikipedia的LRU-K算法优化。

Q4: 业务扩展时如何平滑迁移缓存?

A: 采用双缓存机制:新老缓存同时写入,读取时先读新缓存,未命中再读老缓存并回填新缓存,待新缓存命中率达标后,清理老缓存,微博、知乎的缓存迁移通常采用此方案。

Q5: 小团队没有Redis如何实现缓存?

A: 可先用本地缓存(如Caffeine)+ 数据库查询结果进行简单缓存,注意控制最大容量,避免OOM,当业务增长后,再快速迁移到Redis,本地缓存设计时预留好接口抽象层即可。


缓存设计没有银弹,核心在于理解业务场景的数据特征,建议你在实际系统中配置监控看板(如Grafana + Prometheus),实时观察缓存命中率、延迟和内存使用,根据真实数据动态调整TTL和淘汰策略,一个设计良好的缓存系统应该像人体的淋巴系统——平时默默工作,压力来临时才显现价值。

标签: 缓存设计

抱歉,评论功能暂时关闭!