3. 缓存策略:从浏览器到Redis,层层加速
接口延迟优化,缓存是性价比最高的手段。
我个人习惯,碰到慢接口先不急着动代码,而是问一句:「这数据能不能缓存?」能的话,延迟往往能降一个数量级。
但缓存不是银弹。用错了地方,反而引入数据不一致、内存暴涨、雪崩等新问题。今天我把缓存策略拆成四个层次来讲:浏览器缓存、服务端缓存、Redis缓存,以及最关键的缓存失效策略。
3.1 浏览器缓存:让用户自己帮我们分担压力
浏览器缓存是最靠近用户的一层。说白了,就是让用户的浏览器自己记住之前拿到的资源,下次再请求时直接拿出来用,压根儿不用到服务器。
强缓存 vs 协商缓存,这两个概念必须分清楚:
| 类型 | 响应头 | 行为 | 状态码 |
|---|---|---|---|
| 强缓存 | Cache-Control: max-age=3600 |
时间到期前,浏览器直接从本地读取,不请求服务端 | 200 (from disk cache) |
| 协商缓存 | ETag / Last-Modified |
每次请求都带着标记去服务端验证,资源没变返回304,浏览器用本地缓存 | 304 Not Modified |
我的经验:静态资源(图片、CSS、JS)推荐用强缓存 + 文件名哈希。max-age 可以设到一年。为什么?因为只要文件名变了,浏览器就会当新资源请求。我在某个电商项目里,首页HTML设了30秒强缓存,结果用户反馈改了价格页面不更新——从那以后,HTML页面我坚决不用强缓存,只走协商缓存。
代码配置示例(Nginx):
# 静态资源:强缓存 + 文件名哈希
location /static/ {
expires 365d;
add_header Cache-Control "public, immutable";
}
# HTML页面:只走协商缓存
location / {
etag on;
if_modified_since before;
}
3.2 服务端缓存:别让每一层都去查数据库
浏览器这层没命中,请求到了我们的服务端。这里还有两个常见的缓存入口:
- HTTP 代理缓存:Nginx 的 proxy_cache。适合接口返回结果不频繁变动的场景。我记得有一次,某报表接口每次查询耗时3秒,开 proxy_cache 后降到10毫秒,整个页面加载速度提升显著。
- 进程内缓存:比如 Caffeine (Java), 或者 Go 里的 sync.Map。访问速度极快(微秒级),但只在本进程内有效,多实例部署时注意数据一致性问题。
注意:服务端缓存的 TTL 不宜设太长。我个人习惯把动态接口的不变窗口控制在 5 ~ 60 秒之间。太长的话用户会抱怨数据不实时,太短又起不到加速效果。
3.3 Redis 缓存:分布式场景下的加速核心
进程内缓存存不了多少东西,而且多实例之间不同步。这时候 Redis 就派上用场了。
Redis 缓存是现阶段最流行的选择。速度快(内存操作,微秒级)、数据结构丰富(String、Hash、List、Set、Sorted Set),还支持持久化和集群。
常用的 Redis 缓存模式有两种:
| 缓存模式 | 读策略 | 写策略 | 适用场景 |
|---|---|---|---|
| Cache-Aside | 先查缓存,命中直接返回;不命中查数据库,回写到缓存 | 先更新数据库,再删除缓存(或更新缓存) | 常规业务接口,读多写少 |
| Read-Through / Write-Through | 缓存层代理数据库,应用只跟缓存交互 | 写操作由缓存层同步写入数据库 | 需要自动同步,减少代码侵入 |
避坑指南:写操作时,我曾经直接更新缓存值而不是删除缓存,结果并发场景下出现了数据不一致。后来的做法是:先更新数据库,再删除缓存。下次读请求发现缓存里没有,自然去数据库查最新的,再回写到缓存。这叫“延迟双删”,如果你也面临高并发写,可以考虑加上这个步骤。
Redis 客户端连接池配置很重要,别忽略:
# 以 Java 的 JedisPoolConfig 为例
config.setMaxTotal(50); # 最大连接数
config.setMaxIdle(20); # 最大空闲连接
config.setMinIdle(5); # 最小空闲连接
config.setMaxWaitMillis(3000); # 获取连接超时
config.setTestOnBorrow(true); # 借出前验证连接是否可用
3.4 缓存失效策略:该更新时别犹豫,该淘汰时别手软
缓存不是永不过期。数据总会有变化,内存也总有上限。失效策略决定了缓存系统的稳定性和一致性。
3.4.1 TTL(过期时间)策略
最简单也最常用。给每个 key 设置一个过期时间,时间到了自动删除。
- 固定 TTL:统一到期时间,简单但容易造成缓存雪崩(大量 key 同时过期)。
- 随机 TTL:在基础过期时间上加上一个随机偏移量。比如基础 3600 秒,实际过期时间在 3600 ~ 4200 秒之间随机。我在直播礼物榜单接口上用过这个策略,一下子就把数据库压力降下来了。
// 随机 TTL 示例(伪代码)
long baseTtl = 3600;
long randomOffset = new Random().nextInt(600); // 0~600 秒随机
long actualTtl = baseTtl + randomOffset;
redis.setex(key, actualTtl, value);
3.4.2 内存淘汰策略(Redis 配置)
即使 TTL 设得再合理,内存满了怎么办?Redis 提供了 6 种淘汰策略:
| 策略 | 含义 | 建议场景 |
|---|---|---|
noeviction |
不淘汰,写操作直接报错 | 不推荐线上使用,除非你的内存比数据量大得多 |
allkeys-lru |
在所有 key 中淘汰最近最少使用的 | 通用场景,我的首选 |
volatile-lru |
只在设了过期时间的 key 中淘汰最近最少使用的 | 适合缓存数据 + 持久数据混存的场景 |
allkeys-lfu |
在所有 key 中淘汰最不经常使用的 | 访问频率差异极大的数据 |
volatile-ttl |
淘汰即将过期的 key | 不太推荐,效果不如 LRU |
我曾经历过一次严重故障:线上 Redis 实例配置了 volatile-lru,但业务写入大量不带过期时间的 key,导致缓存空间被长期占满,带过期时间的 key 反而被大量淘汰,请求穿透到数据库……系统直接雪崩。后来我把策略改为 allkeys-lru,并加上了监控告警。我建议你也检查一下自己的 Redis 配置。
3.4.3 主动失效 vs 被动失效
被动失效靠 Redis 自己扫描和 TTL 到期删除,优点是省事,缺点是不及时。主动失效是在业务代码里主动删除或更新缓存。我个人在写操作后都会立刻触发主动失效,保证读请求快速拿到最新数据。
3.5 总结:缓存策略不是单点,而是一套组合拳
光说理论没用。我画了一张图放在本章头上,你可以看看完整的数据流。从浏览器缓存到服务端缓存,再到 Redis 缓存,每一层都承担不同的责任。把它们串联起来,才能让接口响应延迟降到最低。
最后送你一句话:“永远不要只依赖一层缓存。” 多层缓存 + 合理的失效策略 + 监控告警,才是真正的安心之选。