去年年中有一次版本上线之后,我所在的移动端团队连续接了三天线上报警。首页接口的P95耗时从700ms一路涨到2.3秒,数据库连接池反复打满,值班手机凌晨两点还在响。当时第一反应是“是不是新功能写坏了 SQL”,可查了一圈之后发现,问题比想象中更隐蔽——整条首页链路从网络请求到数据库查询再到数据聚合,几乎没有任何一层在做缓存。每一个用户滑动首页,后端都要现查数据库,现算推荐位,现拼聚合数据。在日活几十万的量级下,这种“每次请求都重新造一遍轮子”的架构,迟早会被流量压垮。
当时我做的第一件事,就是把整条首页加载链路完整拆了一遍,目标是搞清楚时间到底花在了哪里。这篇文章就是那次缓存体系改造的完整复盘:客户端缓存怎么分层、数据库侧怎么减负、缓存一致性和三个经典异常怎么处理,以及上线后遇到的四个真实坑。
文章适合正在做移动端 App 性能优化、首页频繁改版、或者被“接口调用慢/数据库压力大”困扰的团队参考。不涉及具体业务代码细节的地方,我会补上通用的设计方案和可复现的排查思路,尽量做到照着就能落地的程度。
1. 首页为什么慢:一次真实请求的耗时拆解与优化突破口
1.1 一次首页请求的时间账单
在动手改任何代码之前,我先在测试环境里抓了一次完整的首页请求链路。用 Charles 抓包配合服务端链路追踪日志,把一次典型的首页刷新拆成了五段:客户端网络建连、DNS 解析、服务端接口处理、数据库查询、数据反序列化与渲染。
那次实测的数据大致是这样:
| 环节 | 耗时 | 占比 | 说明 |
|---|---|---|---|
| DNS + TCP/TLS 建连 | 80ms | 11% | 首次请求无连接复用,弱网下会更严重 |
| 服务端接口处理 | 420ms | 56% | 其中包含 4 次上游 RPC 调用 |
| 数据库查询 | 250ms | 33% | 首页推荐位、用户信息、配置表多次重复查询 |
| 数据传输与 JSON 解析 | 60ms | 8% | Go 端 JSON 序列化 + 客户端反序列化 |
| 客户端渲染 | 40ms | 5% | 列表 diff 与图片占位处理 |
从这里能看得很清楚:真正的瓶颈不在网络,而在服务端接口处理和数据库查询这两块,合计占了接近九成耗时。而这两块恰恰是最适合用缓存机制去优化的位置。首页聚合数据是“读多写少”的典型场景——用户刷新频率远高于后台配置的修改频率,如果每个用户每次刷新都触发一次全量数据库查询,那就是纯浪费。
1.2 为什么“加一台数据库”不是正确答案
团队里有人提出最简单粗暴的方案:数据库加只读从库,把查询压力分摊到多台机器上。这个方案能解决一部分连接数问题,但治标不治本。原因有三个。
第一,首页接口是典型的聚合接口,一次返回可能要拼接用户信息、运营配置、推荐列表、公告等多块数据。哪怕数据库查询本身变快了,服务端还是要把这些数据逐条查询、组装、序列化,再走一遍网络传输。对客户端来说,响应体大、序列化时间长的问题依然存在。
第二,移动端场景和纯 Web 后端不一样,弱网、弱机、信号切换都是常态。后端再快,客户端网络抖动一样会让首页白屏。光在服务端优化数据库,解决不了“地铁里刷不出首页”的用户投诉。
第三,数据库从库扩容成本高,而缓存机制的收益远大于扩容:一次缓存命中,可能直接把一次 250ms 的数据库查询变成 1ms 内的内存读取。这是数量级的差距,加机器很难做到。
所以最终我们定下的优化方向是“客户端缓存降网络 + 服务端缓存降数据库”双管齐下,而不是单纯堆数据库资源。这也对应了我们最早定下的三个目标:首页冷启动首屏时间控制在 2 秒内、接口 P95 降到 1 秒内、数据库 QPS 至少下降 60%。
1.3 一个容易被忽略的索引:冷启动、热启动、回访
移动端首页的缓存设计,必须先区分用户访问的三种场景,因为它们的优化策略完全不同:
- 冷启动:进程被杀后重新打开 App,内存缓存全部丢失,只能依赖磁盘缓存和网络请求;
- 热启动:App 切后台再切回来,进程还在,内存缓存可能仍然有效,体验最好;
- 回访:用户退出后隔几小时或隔天再打开,内存缓存大概率失效,但磁盘缓存还能兜底。
我们的优化重点是冷启动和回访这两种。因为热启动本来就很快,而用户投诉“首页慢”绝大多数发生在冷启动和网络切换场景。后面第三节要讲的磁盘缓存、HTTP 缓存,本质上都是为这两种场景服务的。
2. 客户端三层缓存布局:内存、磁盘与 HTTP 缓存各管一段
2.1 第一层:内存缓存,负责“毫秒级闪开”
内存缓存是整个缓存体系中最快的一层,读取耗时一般小于 1ms。移动端最常用的实现是 Android 的LruCache和 iOS 的NSCache,两者都基于 LRU 策略:最近最少使用的内容优先被淘汰,避免内存无限膨胀。
但我想提醒的是:不要只盯着 LRU 算法,TTL 才是内存缓存最需要设计的参数。首页数据里,有用户强时效性数据(比如未读消息数),也有弱时效数据(比如运营 banner),还有几乎不变的数据(比如 App 版本对应的功能开关)。如果统一用一个 TTL,要么过度刷新造成浪费,要么数据过期时间不一致让用户看到“半新半旧”的页面。
我们当时的做法是给缓存条目打标签,分三档时间:
| 数据类型 | TTL 参考 | 场景举例 |
|---|---|---|
| 强时效 | 30~60 秒 | 未读消息数、用户积分 |
| 中时效 | 5~15 分钟 | 首页推荐位、评论区热门 |
| 弱时效 | 数小时至一天 | 版本配置、隐私协议、城市列表 |
这样用户每次冷启动,App 首屏可以先按“弱时效→中时效→强时效”的顺序读缓存,把首页框架立刻画出来,再在后台异步拉取最新数据更新。这就是“秒开”体验的核心思路。
2.2 第二层:磁盘缓存,负责“冷启动兜底”
内存缓存再好,进程一死就全没了,所以磁盘缓存是冷启动场景的命根子。我们用的是磁盘 LRU 方案,Android 端直接用了DiskLruCache的思路,iOS 端则自己实现了一套基于文件目录 + 最近访问时间清理的缓存管理器。
磁盘缓存的关键不是算法,而是文件格式和序列化方案。很多团队把整个 JSON 字符串直接存进一个文件里,看起来简单,实际上有几个问题:JSON 解析耗时高,大对象容易卡主线程;磁盘文件没有校验,很容易因为写入一半断电导致读取崩溃;明文缓存还有隐私风险。
我们的实践是把响应体按“业务模块”拆开存,每个模块一个缓存条目,序列化用 Protocol Buffers 或者压缩 json 二选一。拆开存的好处是:如果只有推荐位模块过期,就只刷新推荐位,其他模块继续用磁盘缓存,省流量也省时间。隐私方面,涉及用户 ID、手机号等敏感字段的模块直接不入磁盘缓存,只进内存且加密。
磁盘缓存的写入一定要放在子线程。如果用主线程同步写文件,一次 I/O 就可能卡掉 20~30ms,这对首页首帧渲染来说是不可接受的。
2.3 第三层:HTTP 缓存,负责“和服务端对表”
前两层是客户端本地缓存,第三层是客户端与服务端之间的协议级缓存。很多移动端团队会忽略这一层,觉得“服务端已经返回 JSON 了,还要什么缓存”,但实际上 HTTP 缓存能帮你省掉一整轮网络请求。
常规做法是服务端在响应头上返回Cache-Control和ETag。客户端下一次请求可以带上If-None-Match,如果数据没变,服务端直接返回 304 Not Modified,连响应体都不用下传。这个机制在首页接口上效果极好,因为我们 80% 的首页请求,数据其实没有任何变化。
我们用这套方案后,首页接口的重复请求率从 70% 降到了 20% 左右。但要注意,HTTP 缓存只适合 GET 请求,而且不能用于需要严格实时性的数据。如果是用户余额、订单状态这类信息,必须跳过 HTTP 缓存直接回源。
2.4 三级联动的读取顺序:内存 → 磁盘 → 网络回源
把三层缓存串起来的读取逻辑并不复杂,但写的时候很容易漏掉一些边界情况。我们的简化版实现思路是这样:
// 伪代码:首页数据读取流程 fun loadHomeData() { val cached = memoryCache.get(KEY_HOME) // 第一步:读内存 ?: diskCache.get(KEY_HOME) // 第二步:内存没有,读磁盘 ?: networkClient.fetch(KEY_HOME) // 第三步:磁盘也没有,走网络 if (cached != null) { renderHome(cached) } // 网络请求不管有没有命中本地缓存都尽量发,用于刷新数据 refreshAsync() }注意这里有个细节:即使本地缓存命中了,我们也会在后台异步发起一次网络请求,目的是刷新缓存数据。这样用户打开首页立刻能看到旧数据(不需要等网络),过一两秒数据更新了再去增量替换页面。这是“秒开 + 最新数据兼容”的标准做法,也是后面第四节缓存一致性要处理的核心矛盾。
3. 数据库侧减负:查询热点识别与结果复用策略
3.1 先别急着上 Redis,把慢 SQL 找出来再说
我见过太多团队,一提“数据库性能优化”就条件反射地搭一套 Redis,然后把所有查询结果往里塞。缓存不是万能药,它是给“热点查询”和“重复查询”准备的,如果数据库本身的慢 SQL 没解决,就算塞了缓存也只是把慢查询藏到了缓存后面,一旦缓存失效,DB 该崩还是崩。
我们的第一步是打开数据库慢查询日志,把过去一周执行时间超过 200ms 的 SQL 全部拉出来,按“执行次数 × 平均耗时”排序,找出前十名。结果很有意思:榜首是一条反复查最新公告的 SQL,单次执行 380ms,每天执行了 40 多万次,因为每个用户的首页都要查一遍公告,而这个公告内容其实一周才更新一次。这属于典型的“重复查询”,缓存收益巨大。
第二名的 SQL 是首页推荐位列表,每天执行 30 万次,单次 260ms。这个更适合做“预聚合缓存”,因为推荐位的排序规则复杂,SQL 里带了好几个 JOIN 和子查询,每次现算都很贵。
这两条 SQL 占了全库查询量的 22%,把它们优化掉之后,数据库整体 QPS 差不多直接降了三成。所以我的建议是:优化数据库性能的第一步永远是找热点,而不是上缓存组件。
3.2 查询结果缓存:Cache Aside Pattern 的正确姿势
找到热点 SQL 之后,缓存的数据结构也要想清楚。对“查询结果”这种场景,业内最通用的模式是 Cache Aside(旁路缓存),核心流程一句话:读的时候先读缓存,缓存没有就读数据库然后把结果写进缓存;写的时候先更新数据库,再删除旧缓存。
这里有一个当年让我反复踩坑的问题:更新数据库和删除缓存之间,到底哪些地方会出问题?举个具体例子,用户首页的公告缓存 key 是notice:latest,后台运营发了一条新公告,服务端执行了UPDATE notice SET ...,紧接着执行了DEL notice:latest。正常情况下,缓存删除后,下一个请求会回源数据库,读到新公告再写回缓存,一切正常。
但如果删除缓存那一下 Redis 超时了,旧的公告还留在缓存里,可能要在 TTL 过期后才能被替换。最坏情况是用户多看了几个小时旧公告。当时我们引入了一个很实用的补救措施——延迟双删:
- 先删除缓存;
- 更新数据库;
- 隔 500ms~1s 再删一次缓存。
这样做是为了解决“另一个请求在第二步和第三步之间把旧数据重新写回缓存”的并发问题。延迟双删不是银弹,但复杂度低,能在大多数业务场景下把不一致窗口压缩到极小。
3.3 首页聚合数据:直接缓存“组装好的 JSON”,省掉重复计算
除了单条查询结果,首页这种聚合接口还有一个更激进的优化思路:把服务端组装好的整个首页响应体直接缓存起来。也就是说,用户信息、推荐位、公告、配置这些数据,服务端先查齐、组装成最终的 JSON,然后以用户维度或用户分组维度写进 Redis,key 类似home:v2:userId,value 是完整响应体,TTL 设置为 60 秒。
这个方案的效果立竿见影:数据库查询全部省掉,RPC 调用也省了,服务端接口只需要从 Redis 读一次字符串再返回,单次接口耗时从 420ms 降到 30ms 左右,P95 从 2.3s 降到 80ms。
但这个方案也要付出代价——缓存粒度越粗,一致性越难控制。用户数据一改,整个缓存都要失效;运营改一条推荐位,所有人的首页缓存都要清掉。我们当时的妥协方案是:把首页拆成 4~5 个区块,每个区块单独缓存单独 TTL,服务端返回时把多个区块 JSON 片段拼起来。这样“推荐位”变了只需要让推荐位区块失效,用户信息区块还能继续命中缓存。粒度调优是个反复试错的过程,但收益非常直接,非常值得做。
3.4 连接池保护:别让缓存把数据库饿死
最后分享一个容易忽视的细节。加了缓存之后,数据库的查询量大幅下降,但连接池的配置反而需要重新调整。因为回源请求变少了,连接池里大部分连接处于空闲状态,如果还保持原来的 100 个连接,会造成资源浪费;但如果把连接池调得太小,一旦缓存大面积失效,回源流量会瞬间打满连接池,造成连环故障。
我们的做法是保留一个“安全余量”:把连接池从 100 调到 50,但同时给“首页回源”单独设置了熔断阈值——当缓存命中率低于 80% 时,自动把首页接口切回旧数据的“降级版”,优先保住客户端能出页面,而不是让数据库崩溃。
4. 缓存失效与一致性:比缓存本身更值得花时间的设计
4.1 四种失效策略的对比与选型
缓存一致性问题的本质,是数据在数据库和缓存之间存在时间差。没有失效策略的缓存,数据永远是旧的;失效太勤,缓存又失去意义。我们内部对比过四种失效策略:
| 失效策略 | 原理 | 适用场景 | 缺点 |
|---|---|---|---|
| TTL 定时过期 | 缓存写入时指定存活时间 | 大部分读多写少场景 | 时效性不够精准 |
| 主动删除 | 数据更新时主动删除对应缓存 | 强一致性要求场景 | 依赖业务代码埋点 |
| 版本号 | 缓存 key 中加入版本号,版本号变化则缓存失效 | 运营配置类数据 | 版本号需要全局维护 |
| 事件驱动 | 数据库变更通过 binlog/MQ 通知缓存层 | 大规模分布式系统 | 链路长、复杂度高 |
对移动端首页这种场景,我们的组合是“TTL + 主动删除”。具体来说,运营后台只要修改了配置,就发一条消息,服务端收到消息后删除对应缓存 key;如果消息丢失,TTL 兜底保证缓存最终过期。双保险比单纯依赖任何一种都稳。
4.2 先更新数据库,还是先删除缓存?
这是缓存设计里的经典问题。网上有无数讨论,我直接给出结论:
- 绝对不要“先删缓存再更新数据库”。因为更新数据库期间,如果来了一个请求,发现缓存没有,就会去数据库读旧数据再写回缓存,于是缓存里又变成了旧值,且这个旧值会一直存活到 TTL 过期。
- 正确顺序是“先更新数据库,再删除缓存”。这个顺序也有并发窗口,但窗口极小,配合延迟双删,基本可以接受。
当年压测时我们用了一个很凑巧的复现方式:更新公告接口,同一个 key 并发来了 100 个读取请求。在“先删缓存再更新 DB”的旧逻辑下,几乎所有请求都打到了数据库,而且返回结果一半旧一半新。后来改成“先更新 DB 再删缓存”并加了延迟双删,同样压测,只有极少数请求会命中旧缓存,业务层面完全可以接受。
4.3 数据库变更如何通知缓存层:binlog 订阅方案
如果团队业务复杂,靠业务代码到处手动删缓存,很容易漏。我们的服务端在运营后台和用户中心两个系统里尝试了事件驱动:用 Canal 订阅数据库 binlog,把变更事件推到 MQ,消费端根据变更表名和主键拼出缓存 key,自动删除对应缓存。
这个方案的优点是业务代码零入侵——不需要在 UPDATE 语句后面手动加 DEL 命令;缺点是链路长,Canal 挂了会影响缓存更新时效。我们当时的落地策略是分阶段:先只在“公告表”和“推荐位表”这两张高频变更且强一致要求的表上启用 binlog 订阅,其余表继续用显式删除 + TTL 兜底。跑了两周,稳定性足够,才逐步扩大到更多表。
4.4 移动端场景的一致性妥协:最终一致,把时间差控制在可接受范围
移动端和服务端最大的不同在于:用户手机上的数据,不管服务端怎么推,都有天然的“滞后”。所以移动端缓存设计不应该追求强一致,而是追求“最终一致 + 可感知的刷新时机”。
我们的首页策略是:用户冷启动后先展示本地磁盘缓存,同时触发服务端刷新,服务端返回新数据后,客户端做增量 diff 更新。用户看到的现象是“页面秒开,大约半秒后数字跳动变成最新”。这个体验用户普遍能接受,反而比白屏等 2 秒更舒服。
要留意的是增量更新不能做得太激进。我们曾经尝试每次刷新都全量对比列表,结果低端机上列表 diff 耗时 200ms,卡顿明显。后来改成只在服务端响应 header 里放一个dataVersion,客户端比对版本号,版本变了才做 diff,版本不变就完全不动 UI。
5. 防穿透、防击穿、防雪崩:三个必须提前铺好的底线
5.1 缓存穿透:查询了一个永远不存在的数据
缓存穿透指的是请求的数据在数据库和缓存中都不存在,每次请求都直接打到数据库。移动端最常见的情况是:用伪造的用户 ID、越权的商品 ID 去请求,或者用户刚删除了某条内容但客户端还在用旧 ID 请求。如果没有防护,这类请求可以绕过缓存直接压垮数据库。
防护手段有两种,我们都在用:
- 缓存空值:查询数据库发现结果为空,也把这个空结果写入缓存,TTL 设短一点,比如 30~60 秒。后续同样的请求直接命中空缓存,不会打到数据库。
- 布隆过滤器:在缓存和数据库之间加一道过滤器,请求的 key 如果不在过滤器中,直接返回默认值。布隆过滤器的优势是内存占用极小,几百万个 key 只需要几十 MB;缺点是存在小概率误判,但误判方向是“存在可能查不到”,对业务影响很小。
5.2 缓存击穿:热点 key 在过期瞬间被打爆
缓存击穿和穿透一字之差,但机制完全不同:击穿指的是一个热点 key 正好在过期时间被大量请求同时访问,所有请求都发现缓存没有,瞬间全部回源数据库,数据库直接被打挂。
移动端首页的“公告配置”“全局开关”这类 key 就是典型的热点 key——所有用户的首页都会读它。我们的做法是给回源过程加互斥锁:Redis 没有命中时,先尝试获取一把分布式锁,只有拿到锁的请求才允许去数据库查询并回填缓存,其他请求短暂 sleep 后重新读缓存。代码思路如下:
# 伪代码:缓存击穿防护 value = redis.get(key) if value is None: if redis.setnx(lock_key, "1", ex=5): # 尝试加锁 value = db.query(...) redis.set(key, value, ex=600) redis.delete(lock_key) # 释放锁 else: time.sleep(0.05) value = redis.get(key) # 其他人等锁释放后重试这个方案会稍微增加单次首访延迟,但保证了数据库同时最多只有一个热点查询在跑,收益远大于代价。需要注意 lock_key 必须设置过期时间,否则某个请求拿锁后进程崩溃,锁永远不会释放,会造成死锁。
5.3 缓存雪崩:大量 key 同时过期
雪崩是击穿的放大版:大量缓存 key 在同一时间段过期,导致大量请求同时回源数据库。最常见的原因是统一把 TTL 设置成一样的时间,比如所有 key 都是 10 分钟,那么每 10 分钟就有一次回源高峰。
解决方法非常朴实:给 TTL 加随机扰动。比如基础 TTL 设为 600 秒,实际设置时加 0~300 秒随机值。这样 key 的过期时间自然散开,不会出现“准时打群架”的场景。另外,多级缓存也会在雪崩时起保护作用——客户端本地还有内存和磁盘缓存,服务端 Redis 挂了,客户端也不至于白屏。
5.4 给首页场景的配置参考
结合上面的原理,我当时给团队产出了一份可直接用的配置清单:
| 场景 | key 示例 | TTL | 防护手段 |
|---|---|---|---|
| 公告/运营配置 | config:notice | 300~600s + 随机 60s | 主动删除 + 互斥锁 |
| 首页聚合碎片 | home:block:${blockId} | 60s + 随机 10s | 延迟双删 + 兜底降级 |
| 用户信息 | user:${userId} | 30s | 主动删除 + 空值缓存 |
| 缓存穿透黑名单 | blocklist:${uid} | 不设 TTL | 布隆过滤器 |
这套配置不是固定的,要根据业务访问模型调。但核心思想是一致的:热点 key 必须有击穿保护,所有 key 的过期时间必须随机化,空结果也必须被缓存。把这三点做到,三个经典故障就已经挡住了一大半。
6. 改造后的实测数据与踩过的四个坑
6.1 改造前后数据对比
整个改造持续了三周,分三批上线:第一批客户端内存和磁盘缓存,第二批服务端聚合缓存和热点查询缓存,第三批布隆过滤器和延迟双删。全部上完一周后,我们拉了监控数据:
| 指标 | 改造前 | 改造后 | 变化 |
|---|---|---|---|
| 首页冷启动首屏时间 | 3.8s | 1.4s | 下降 63% |
| 首页接口 P95 | 2.3s | 80ms | 下降 96% |
| 首页接口 P99 | 4.1s | 320ms | 下降 92% |
| 数据库整体 QPS | 12000 | 4100 | 下降 66% |
| DB 连接池使用率 | 96%(报警) | 31% | 恢复正常 |
| 用户“首页加载失败”反馈 | 日均 30+ | 日均 3 | 明显改善 |
最直观的感受是:之前随手打开首页总要先转圈半秒到一秒,改造完之后基本是“点到即出”,这一点连产品经理和测试同事都主动来问做了什么。
6.2 坑一:缓存存大对象,GC 和网络双双受伤
我们最早把整个首页 JSON 作为一个 value 存进了 Redis,一个 value 就有 60KB。一开始没觉得有什么问题,等并发上来后发现两个现象:服务端 Go 进程的 GC 明显变频繁,因为每次从 Redis 反序列化 60KB JSON 对堆内存压力不小;同时客户端弱网环境下下载 60KB 也比预想中慢。
后来我们做了两个改变:一是把首页拆成多个区块缓存,每个区块控制在 10KB 以内;二是把 JSON 换成更紧凑的序列化格式。这个教训就是:缓存设计的性能瓶颈往往不在存储,而在序列化和网络传输。大对象缓存要慎重,能拆则拆。
6.3 坑二:热 key 问题,单 key 扛了 90% 流量
上线后我们发现 Redis 有一个 key 的访问量占了总访问量的九成——就是全局公告配置那个 key。虽然 Redis 单 key 抗压能力强,但持续高并发访问同一个 key 会有两个隐患:一是 Redis 单分片 CPU 会飙高;二是如果这个 key 一旦失效,击穿效应会非常猛。
我们最终的解法是“key 分片”:把同一个数据复制成 16 份,分别存储为notice:v1:1、notice:v1:2一直到notice:v1:16,读取时按用户 ID hash 到某一个分片。这样单 key 的并发量直接降到原来的 1/16。代价是数据更新时要写 16 份,但对低频更新的配置数据来说,完全值得。
6.4 坑三:缓存空值导致“僵尸数据”僵了好几天
有一段时间我们遇到一个诡异的问题:某条新闻已经下架三天了,但部分用户首页还能看到。排查了很久,最后发现是之前提到的“空值缓存”和“主动删除”之间产生了冲突——
流程是这样:那条新闻下架时,服务端主动删除了news:${id}的缓存;但删除前刚好有一个请求发现缓存没有,回源数据库也没查到(因为已经下架了),按防穿透策略往缓存里写了一个空值,TTL 设了 3 天。于是后续所有请求都命中空值缓存,新闻就“消失”了,从用户侧看就是“下架了”,从业务侧看是正常行为。但问题出在另一些请求用了旧版本客户端,客户端本地磁盘缓存还存着这条新闻,服务端缓存虽然下架了,客户端却不会主动清,就造成“下架三天还在显示”的假象。
这个坑的教训有两条:一是空值缓存 TTL 不能设太长,我们后来统一改成 30 秒;二是客户端本地磁盘缓存必须有版本控制,服务端数据版本变化时要能强制客户端清理旧的本地缓存。
6.5 优化上线后的日常:三个每天要看一眼的监控项
最后分享我现在每天都会看一眼的三个监控项,建议做缓存优化的团队也把它们加到告警里:
- 缓存命中率:服务端整体命中率低于 85% 时排查是否出现了缓存穿透或大量新 key 涌入;
- Redis 内存增长曲线和 key 过期数量:内存陡增可能是大对象写入,key 瞬时集中过期可能是雪崩前兆;
- 回源数据库 QPS 曲线:如果某个时间点回源 QPS 突然升高,配合缓存命中率一起看,基本能定位到是哪个 key 出了问题。
我个人在实际操作中的体会是:缓存机制从来不是一个“加一个 Redis 就万事大吉”的技术点,它更像是一个贯穿客户端、服务端、数据库三端的系统工程。每一次 TTL 设置、每一个 key 的设计、每一层缓存的失效策略,背后都是一次对业务场景的重新理解。上面这些方法和坑,都是我们在真实流量下用线上故障换来的,希望能帮你在做移动端首页优化时少走几段弯路。