艾体宝干货 | 【Redis实用技巧#8】缓存层的背后,不同的选择

27 阅读7分钟

当业务流量持续增长,应用变得越来越慢。有人开始盯着数据库监控看:CPU 长时间打满、慢查询暴增、连接数不断堆积。于是尝试纵向扩容服务器,扩了一次又扩一次。

给表加索引、调参数、改 SQL,但问题始终没有根本缓解,数据库变成了单点、昂贵且无法回避的性能瓶颈

再仔细一看,问题其实很明显:

数据库在反复做同样的事情。

  • 同一个用户资料查询
  • 同一份「热门商品」榜单
  • 同一个仪表盘里那条代价极高的聚合 SQL

缓存层(caching layer)和查询加速(query acceleration)存在的核心原因只有一个: 不要一遍又一遍地让数据库解同一道题。

本文将围绕三类最常见、也最关键的手段展开:

  • Redis
  • Memcached
  • Materialized View(物化视图)

同时也会讨论一个现实问题:要更快,从来不是没有成本的,真正的坑往往藏在细节里。


缓存有效的前提

从理论上看,缓存的逻辑非常简单:

  • 如果某个计算成本高、访问频率又很高,就把结果存到一个“很快的地方”
  • 下次再请求时,直接返回这个结果,而不是重新计算

但有问题的地方在于三个问题:

  1. 这个“很快的地方”放在哪里?
  2. 缓存数据应该保留多久?
  3. 底层数据发生变化时,缓存怎么办?

在实际的系统中,反复出现的主要有三种模式:

  1. 应用内缓存(request-level / process-level)
  2. 外部 KV 缓存(Redis、Memcached)
  3. 数据库层面的加速机制(如 Materialized View)

它们解决的是不同层面的问题,也并非相互替代。


Redis 与 Memcached:为热点数据准备的挂载

Redis 和 Memcached 都属于内存型 Key-Value 存储

核心思想一致: 用极低延迟的内存访问,换掉高成本的数据库查询。

差异主要体现在能力边界上:

  • Memcached

    • 设计极度简单
    • 只做一件事:缓存任意二进制数据或序列化对象
  • Redis

    • 支持更丰富的数据结构(string、hash、list、set、sorted set、stream 等)
    • 支持持久化与更复杂的功能

典型使用场景

  • 缓存用户会话(session)、用户资料、权限信息
  • 缓存接口返回结果或已渲染的 HTML 片段
  • 缓存高成本计算结果(例如推荐结果)

一个非常典型的缓存模型如下:

  • Keyuser:123:profile
  • Value:用户资料的 JSON
  • TTL(过期时间) :5 分钟

请求流程通常是:

  1. 应用先查缓存:是否存在 user:123:profile
  2. 命中(cache hit):直接返回
  3. 未命中(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 与业务认知脱节

财务认为今天的数据已包含,但视图其实是昨晚刷新的。 应对方式:

  • 在报表中明确展示刷新时间
  • 刷新策略与业务口径对齐

下次面临类似的处境,不妨思考一个简单的问题:如果列出系统中所有重复计算相同答案的地方,其中有多少,本应从立项之初起就被设为应该缓存?