# 缓存设计
作者:Ethan.Yang
博客:https://blog.ethanyang.cn (opens new window)
上一篇文章介绍了并发与分布式的基本概念,本篇将深入讲解系统中不可或缺的性能优化手段 —— 缓存设计。
# 一、缓存的核心思想
缓存的本质是以空间换时间。
通过将计算或查询结果暂存起来,减少后续访问的计算与IO成本,从而提升系统响应速度。
- 目标:节省时间、降低系统压力。
- 成本:增加内存或磁盘空间。
- 核心:提高命中率(Hit Rate)。
缓存不仅仅是 Redis、Map 等存储,它是一种系统性思维:
将复杂操作结果转化为简单查询的过程,就是一种“导流”优化。
# 二、缓存命中率与收益
假设:
- 命中率 = 命中的请求数 / 总请求数
- 查询总耗时 = 缓存查询时间 + (1 - 命中率) × 原始查询时间
显然,提升缓存收益有两种途径:
- 降低缓存访问成本(例如减少序列化、网络延迟等);
- 提高命中率(例如合理设计 Key、缓存粒度、过期策略)。
缓存最适合用于:
- 读多写少的场景;
- 原始查询耗时较长的场景。
# 三、Key 的设计原则
# 1. 唯一性与避免碰撞
不同业务必须使用不同的 Key 前缀,否则容易被覆盖。
Key 应该唯一、稳定且易区分。
推荐命名规范:
系统标识:功能标识:业务标识 示例:user:login:13910712345
# 2. 高效生成与比较
- Key 计算不宜过复杂;
- 保证快速的 equals 与 hashcode;
- 可使用单向哈希(如 MD5、SHA-256)简化复杂参数映射。
# 3. 层次结构
Key 设计要支持分层、可溯源,避免“无意义随机串”。
# 四、Value 的设计与序列化
缓存的值可分为两类:
- 对象值(反序列化后直接使用)
- 二进制值(节约内存、网络传输快)
需要注意:
- 数据污染:缓存的数据不是最新值(例如写未同步)。
- 缓存本质上是数据调用方与数据提供方之间的中间层。
# 五、缓存更新机制
# 1. 时效性更新(被动)
- 每条数据设定 TTL,到期后自然失效;
- 读取时若缓存失效,再从数据源加载并写回缓存;
- 放弃实时一致性,适用于浏览量、点赞数、关注人数等。
# 2. 主动更新(Cache Aside)
写入时:
- 先更新数据库;
- 再删除缓存(而非更新缓存)。
删除缓存能避免旧数据被覆盖新值。
但仍可能存在读写竞争的极小概率不一致(可忽略,称为“鸵鸟算法”)。
# 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 发布订阅、消息队列、批处理任务;
- 核心思路:削峰填谷。
适用场景:
- 秒杀、抢购、红包、推送系统;
- 峰谷差明显、对实时性要求不高的业务。
收益计算:
原始时间 > 写缓存时间 + 异步传递时间
⇒ 用户感知响应更快。