# 缓存设计

作者:Ethan.Yang
博客:https://blog.ethanyang.cn (opens new window)


上一篇文章介绍了并发与分布式的基本概念,本篇将深入讲解系统中不可或缺的性能优化手段 —— 缓存设计

# 一、缓存的核心思想

缓存的本质是以空间换时间
通过将计算或查询结果暂存起来,减少后续访问的计算与IO成本,从而提升系统响应速度。

  • 目标:节省时间、降低系统压力。
  • 成本:增加内存或磁盘空间。
  • 核心:提高命中率(Hit Rate)。

缓存不仅仅是 Redis、Map 等存储,它是一种系统性思维

将复杂操作结果转化为简单查询的过程,就是一种“导流”优化。

# 二、缓存命中率与收益

假设:

  • 命中率 = 命中的请求数 / 总请求数
  • 查询总耗时 = 缓存查询时间 + (1 - 命中率) × 原始查询时间

显然,提升缓存收益有两种途径:

  1. 降低缓存访问成本(例如减少序列化、网络延迟等);
  2. 提高命中率(例如合理设计 Key、缓存粒度、过期策略)。

缓存最适合用于:

  • 读多写少的场景;
  • 原始查询耗时较长的场景。

# 三、Key 的设计原则

# 1. 唯一性与避免碰撞

不同业务必须使用不同的 Key 前缀,否则容易被覆盖。
Key 应该唯一、稳定且易区分。

推荐命名规范

系统标识:功能标识:业务标识 示例:user:login:13910712345

# 2. 高效生成与比较

  • Key 计算不宜过复杂;
  • 保证快速的 equals 与 hashcode;
  • 可使用单向哈希(如 MD5、SHA-256)简化复杂参数映射。

# 3. 层次结构

Key 设计要支持分层、可溯源,避免“无意义随机串”。

# 四、Value 的设计与序列化

缓存的值可分为两类:

  • 对象值(反序列化后直接使用)
  • 二进制值(节约内存、网络传输快)

需要注意:

  • 数据污染:缓存的数据不是最新值(例如写未同步)。
  • 缓存本质上是数据调用方与数据提供方之间的中间层

# 五、缓存更新机制

# 1. 时效性更新(被动)

  • 每条数据设定 TTL,到期后自然失效;
  • 读取时若缓存失效,再从数据源加载并写回缓存;
  • 放弃实时一致性,适用于浏览量、点赞数、关注人数等。

# 2. 主动更新(Cache Aside)

写入时:

  1. 先更新数据库;
  2. 再删除缓存(而非更新缓存)。

删除缓存能避免旧数据被覆盖新值。
但仍可能存在读写竞争的极小概率不一致(可忽略,称为“鸵鸟算法”)。

# 3. 延迟双删策略(常用)

  1. 删除缓存;
  2. 更新数据库;
  3. 延迟一段时间后再次删除缓存。
    若第二次删除失败,可通过消息队列重试实现补偿。

# 4. Read/Write Through

  • 写操作直接写入缓存,由缓存同步至数据库;
  • 读操作仅访问缓存;
  • 初始化时预加载数据(缓存预热)。

特点:实现简单但要求缓存极高可用性。

# 5. Write Behind(异步写回)

  • 写入缓存后异步更新数据库;
  • 借助消息队列保证最终一致性;
  • 适用于写频繁但对实时一致性要求不高的系统。

# 六、缓存清理机制

缓存清理的目标是在有限空间内最大化命中率

# 1. 时效性清理

为每条缓存设置 TTL,到期自动清理。

  • 可通过轮询扫描(类似定时任务);
  • 或自动触发(如 Cookie 的自然过期)。

# 2. 容量阈值清理

当缓存数量或大小超过上限时触发:

  • FIFO(先进先出)
  • LRU(最近最少使用)
    → Java 中可用 LinkedHashMap 实现
  • LFU(最少使用次数)

# 3. 引用类型与内存回收

  • 强引用:不会被 GC 回收;
  • 软引用(SoftReference):内存不足时回收;
  • 弱引用/虚引用:更积极的回收。

组合策略:

LRU + 软引用 = 保留最近使用数据,同时防止内存溢出。

# 七、缓存风险与应对策略

# 1. 缓存穿透

查询的 Key 不存在于缓存和数据库中,导致每次都打到数据库。

解决

  • 缓存空值;
  • 增加布隆过滤器拦截非法 Key。

# 2. 缓存雪崩

大量缓存同时过期,数据库瞬间承压。

解决

  • 设置随机过期时间(错峰失效);
  • 增加多级缓存;
  • 关键数据异步刷新。

# 3. 缓存击穿

热点 Key 突然失效,被大量请求同时击穿到数据库。

解决

  • 使用互斥锁(Mutex)控制热点 Key 重建;
  • 对热点数据永不过期;
  • 结合 LRU 保证热点数据常驻内存。

# 4. 缓存预热

系统启动或缓存大面积失效后,提前加载关键数据。
可通过脚本、定时任务或消息通知触发预热,提升启动阶段的命中率。

# 八、缓存的层级与部署位置

# 1. 客户端缓存

  • 浏览器 Cookie、LocalStorage;
  • App 端 SQLite、本地文件;
  • 适合轻量级、静态类数据(配置、引导页等)。

# 2. CDN 缓存(边缘缓存)

  • 主要缓存静态资源:图片、脚本、页面;
  • 也可缓存通用静态数据(如地名、行业分类、字典数据)。

# 3. 服务端缓存

  • 内存缓存:Guava Cache、LocalCache;
  • 分布式缓存:Redis、Memcached;
  • 靠近业务逻辑层,减少数据库压力。

# 4. 数据库层缓存

  • 冗余字段、中间表;
  • 异步统计、延迟聚合;
  • 适用于非实时数据(如订单统计)。

# 九、写缓存与削峰限流

读缓存解决“高并发读取”,而写缓存解决“高并发写入”:

  • 写缓存位于调用方与数据处理方之间
  • 常见实现:Redis 发布订阅、消息队列、批处理任务;
  • 核心思路:削峰填谷

适用场景:

  • 秒杀、抢购、红包、推送系统;
  • 峰谷差明显、对实时性要求不高的业务。

收益计算:
原始时间 > 写缓存时间 + 异步传递时间
⇒ 用户感知响应更快。