当业务流量持续增长,应用变得越来越慢。有人开始盯着数据库监控看:CPU 长时间打满、慢查询暴增、连接数不断堆积。于是尝试纵向扩容服务器,扩了一次又扩一次。
给表加索引、调参数、改 SQL,但问题始终没有根本缓解,数据库变成了单点、昂贵且无法回避的性能瓶颈。
再仔细一看,问题其实很明显:
数据库在反复做同样的事情。
- 同一个用户资料查询
- 同一份「热门商品」榜单
- 同一个仪表盘里那条代价极高的聚合 SQL
缓存层(caching layer)和查询加速(query acceleration)存在的核心原因只有一个: 不要一遍又一遍地让数据库解同一道题。
本文将围绕三类最常见、也最关键的手段展开:
- Redis
- Memcached
- Materialized View(物化视图)
同时也会讨论一个现实问题:要更快,从来不是没有成本的,真正的坑往往藏在细节里。
缓存有效的前提
从理论上看,缓存的逻辑非常简单:
- 如果某个计算成本高、访问频率又很高,就把结果存到一个“很快的地方”
- 下次再请求时,直接返回这个结果,而不是重新计算
但有问题的地方在于三个问题:
- 这个“很快的地方”放在哪里?
- 缓存数据应该保留多久?
- 底层数据发生变化时,缓存怎么办?
在实际的系统中,反复出现的主要有三种模式:
- 应用内缓存(request-level / process-level)
- 外部 KV 缓存(Redis、Memcached)
- 数据库层面的加速机制(如 Materialized View)
它们解决的是不同层面的问题,也并非相互替代。
Redis 与 Memcached:为热点数据准备的挂载
Redis 和 Memcached 都属于内存型 Key-Value 存储。
核心思想一致: 用极低延迟的内存访问,换掉高成本的数据库查询。
差异主要体现在能力边界上:
-
Memcached
- 设计极度简单
- 只做一件事:缓存任意二进制数据或序列化对象
-
Redis
- 支持更丰富的数据结构(string、hash、list、set、sorted set、stream 等)
- 支持持久化与更复杂的功能
典型使用场景
- 缓存用户会话(session)、用户资料、权限信息
- 缓存接口返回结果或已渲染的 HTML 片段
- 缓存高成本计算结果(例如推荐结果)
一个非常典型的缓存模型如下:
- Key:
user:123:profile - Value:用户资料的 JSON
- TTL(过期时间) :5 分钟
请求流程通常是:
- 应用先查缓存:是否存在
user:123:profile - 命中(cache hit):直接返回
- 未命中(cache miss):查数据库 → 写入缓存 → 返回结果
对于读多写少的接口,这一层缓存往往可以成数量级地降低数据库压力。
常见问题(也是事故高发区)
- 数据不新鲜(stale data) :缓存落后于数据库
- 缓存失效(invalidation) :用户更新资料后,旧缓存未被清理
- 内存受限:LRU 等淘汰策略可能在高峰期踢掉关键数据
缓存很好加,但非常容易在细节上出错。
实践中,最常见的两种策略是:
-
短 TTL
- 接受轻微不一致
- 依靠自然过期兜底
-
写时失效
- 所有写操作必须同步更新或删除相关缓存
对于高价值、用户可感知的数据,通常需要两者结合使用。
不只是缓存,优势与风险并存
Redis 值得单独强调,因为它远不止是一个缓存工具。它支持的能力包括:
- Hash:非常适合存储用户属性
- Sorted Set:排行榜、权重排序
- Pub/Sub:轻量级消息机制
- Stream:事件流与日志
因此,在很多团队中,Redis 会被用于:
- 限流(基于计数器)
- 后台任务队列
- 实时指标与信息流
这种灵活性非常强,但也意味着责任边界必须足够清晰。一旦 Redis 承载了过多不可丢失的关键状态,它就不再是“缓存”,而变成了核心系统组件:
- 故障恢复更复杂
- 部署与升级风险显著上升
设计时必须明确区分:
- 哪些数据 丢了也没关系
- 哪些数据 必须有持久化保障
相比之下,Memcached 的定位非常克制:不做持久化、不承担状态,只专注缓存。在纯 Web 响应缓存或对象缓存场景中,这种简单反而是优势。
Materialized View:数据库内部的预计算解法
与 Redis、Memcached 不同,Materialized View(物化视图)存在于数据库内部。需要先区分两个概念:
- 普通 View:只是保存了一条 SQL,每次查询都会重新执行
- Materialized View:会把查询结果真实存储下来
查询 Materialized View 时,读取的是预先计算好的结果,而不是实时跑聚合。
一个典型场景
你有一张订单表,数据量巨大。管理层的仪表盘需要展示:
- 按天
- 按国家
- 汇总的营收数据
直接查原始表,每次都要扫描上百万行。解决方式是:
- 创建一个 Materialized View
- 预先计算「每日 / 每国家的营收汇总」
- 每 5 分钟或每小时刷新一次
仪表盘直接查询该视图,响应几乎是瞬时的,数据库负载也明显下降。
需要权衡的点
- 数据时效性:只和最近一次刷新时间一样新
- 维护成本:刷新本身会消耗 CPU 与 I/O
- 复杂度差异:不同数据库对增量刷新支持程度不同
刷新策略必须和业务需求匹配:
- 接近实时指标 → 高频刷新,成本高
- 日报 / 周报 → 夜间刷新,成本低
对于分析型负载与复杂报表,Materialized View 往往比“在应用层缓存每条 SQL”更清晰、也更可控。
额外的视角:性能不只是交给后端实现
缓存并不仅是工程优化,而是产品层面的选择。
任何缓存,本质上都是在做取舍:
- 用户资料是否必须全局实时?
- 分析数据延迟 5 分钟可以吗?1 小时呢?
- 库存状态是否必须 100% 实时?
Redis、Memcached、Materialized View,都是对这些问题的不同回答。
如果产品侧同时要求:
- 所有数据全局实时
- 无限扩展
- 成本可控
那其实是在描述一个并不存在的解法。
一旦达成清晰共识,设计反而更简单:
- 强一致、关键路径:少缓存,直接读源数据
- 非关键用户路径:积极缓存、允许轻微延迟
- 内部工具与分析:重度预计算,弱实时
否则,就很容易出现两种极端:
- 缓存过度 → 出现数据问题
- 缓存不足 → 硬件成本失控
常见的问题
很多系统偶发变慢或偶尔数据不对的问题,往往源于以下模式。
Cache Stampede(缓存雪崩)
热点 Key 同时过期,大量请求瞬间打到数据库。 应对方式:
- 给 TTL 增加随机抖动,避免同时过期
- 使用「锁 + 回填」机制:只允许一个请求重建缓存,其余请求返回旧值
静默的旧数据
用户已更新数据,但缓存未失效,长时间看到旧内容。 应对方式:
- 写路径必须强制触发缓存失效
- 用户可感知数据使用合理 TTL
把缓存当成事实来源
代码只更新 Redis,忘记写数据库。 一次重启,数据全部消失。 应对方式:
- 明确 system of record(事实来源)
- 缓存必须被视为可丢弃层
Materialized View 与业务认知脱节
财务认为今天的数据已包含,但视图其实是昨晚刷新的。 应对方式:
- 在报表中明确展示刷新时间
- 刷新策略与业务口径对齐
下次面临类似的处境,不妨思考一个简单的问题:如果列出系统中所有重复计算相同答案的地方,其中有多少,本应从立项之初起就被设为应该缓存?