热点处理

多级缓存架构

系统采用多级缓存架构来保护供应商 API:

多级缓存

L1

本地缓存

应用进程内存缓存
L2

Redis 缓存

分布式缓存服务
L3

供应商 API

原始数据源(第三方)

读写分离上线后,系统运行正常。

但 Redis 集中式缓存的架构很快暴露了瓶颈:

  • 单点压力:所有请求最终都会汇聚到同一个 Redis 分片,热点 API 的 QPS 直接转化为 Redis 单节点的 CPU 和网络开销
  • 网络延迟:每次缓存查询都需要一次网络 RTT(1~5ms),即使命中也要付出这个代价
  • 供应商 API 成本:如果 Redis 未命中,大量并发请求直接打到供应商 API,触发限流甚至产生额外计费

为了解决这些问题,我们引入了 L1 本地缓存作为第一道防线——在应用进程内存中直接返回热点数据,消除网络开销,大幅削减对 Redis 和供应商 API 的冲击。

选型边界
为什么在 Redis 前面再加 L1 本地缓存
触发问题
热点 API 即使命中 Redis,也会把所有请求压到同一条网络链路和同一批 Redis 分片上。
候选方案
继续扩 Redis 分片、CDN 缓存、本地缓存、热点 key 分散、主动预热。
选择理由
L1 本地缓存可以在应用进程内直接返回热点数据,最快降低 Redis QPS,也不改变外部 API 的调用模型。
代价
每台实例都有自己的缓存副本,必须接受短时间不一致,并处理冷启动和批量失效问题。
暂不解决
暂不把所有 API 都放进本地缓存,只针对可短暂过期、读多写少、热点明显的数据。

突发新闻

但某天,一个突发新闻事件发生了:

突发事件

事件:某重大新闻突发
影响:
新闻 API 调用量暴增 10 倍
Redis 中某个热点 key 被频繁访问
Redis 单节点压力过大

调用流程分析

正常请求调用流程(多级缓存)
用户发起 API 调用
客户端请求到达
到达应用层
负载均衡分发到应用服务器
检查 L1 本地缓存
进程内存中有该数据吗?
L1 命中
直接返回本地缓存数据
响应时间:~1ms
L1 未命中
继续查询 L2 Redis 缓存
跨实例共享缓存
检查 L2 Redis 缓存
Redis 中有该数据吗?
L2 命中
返回 Redis 缓存数据
响应时间:~5ms
回写 L1 本地缓存
预热进程内存
L2 未命中
继续查询 L3 供应商 API
向外部服务商发起请求
获取供应商响应数据
第三方 API 返回
写入 L2 Redis 缓存
设置过期时间
回写 L1 本地缓存
预热进程内存
返回给用户

问题分析

我查了一下监控:

监控分析

Redis 访问统计(QPS 分布)
Key #0
855 QPS
855
Key #1
633 QPS
633
Key #2
739 QPS
739
Key #3
5000 QPS
5000
Key #4
967 QPS
967
Key #5
521 QPS
521
Key #6
776 QPS
776
Key #7
887 QPS
887
Key #8
553 QPS
553
Key #9
519 QPS
519
#0
#1
#2
#3
#4
#5
#6
#7
#8
#9

多级缓存系统工作正常,但单个热点 Key 仍然导致 Redis 单点压力。

虽然 L1 本地缓存挡掉了一部分请求,但突发流量远超预期,且冷启动的实例 L1 是空的,请求直接打到了 Redis 上。

我的思考

既然一个 Redis 实例扛不住,是否有一个机制,让流量分散到多个 Redis 实例上,而且支持增减实例?就像这样:

多实例动态分流架构

海量并发请求 QPS 10w+
路由层
Redis 分片 1
内存: 8GB
Redis 分片 2
内存: 8GB
Redis 分片 3
内存: 8GB
动态扩容
在线添加节点

我去阿里云看看有没有这样的方案,果然看到了这样的选项:

架构类型
集群版
标准版

原来云厂商早就把这套机制产品化了——集群版就是我们要找的方案。选择集群版后,系统自动创建多个分片实例,流量通过内置代理自动打散,还支持随时增加分片数量。

但问题来了,有了集群版就能高枕无忧了吗?并不。集群版虽然能把不同 Key 分散到不同分片,但同一个热点 Key 仍然只会落在一个分片上。这就是为什么我们还需要下面的”热点 Key 分散”方案。

解决方案

1. 热点 Key 分散

假设某个热点新闻的ID是:88,我将新闻存到Redis时,原来是直接用这个ID作为Key:

news:88 -> { content: "...", timestamp: "..." }

假设现在我有了 3 个 Redis 分片,但我的热点 Key 仍然只落在其中一个分片上,导致这个分片压力过大。有没有办法把这个热点 Key 分散到多个分片上呢?

简单,我通过某种算法,将不同的请求映射到不同的 Key 上:

news:0:88 -> { content: "...", timestamp: "..." }
news:1:88 -> { content: "...", timestamp: "..." }
news:2:88 -> { content: "...", timestamp: "..." }
...
news:9:88 -> { content: "...", timestamp: "..." }

这 10 个 Key 就可以分布到不同的 Redis 分片上了。

热点 Key 分散

原理: 按请求级变量(user_id / request_uuid)做 Hash 取模,将同一个热点新闻的请求分散到 10 个子 Key
request = user:B3 hash(user:B3) % 10 = 8 news:88:8
请求分布到 10 个子 Key:
news:88:0
news:88:1
1 请求
news:88:2
news:88:3
1 请求
news:88:4
news:88:5
news:88:6
news:88:7
1 请求
news:88:8
1 请求
news:88:9
1 请求
云厂商 Redis 集群 内置 3 个物理分片
分片 1
news:88:2 news:88:5 news:88:8
分片 2
news:88:0 news:88:3 news:88:6 news:88:9
分片 3
news:88:1 news:88:4 news:88:7
说明: 10 个子 Key 通过 Redis 内部的 Hash 算法自动路由到集群中的不同物理分片,实现流量打散。

2. 本地缓存预热

另外,我发现,我的新闻供应商提供了一个 API,可以查询最近的热点新闻列表。这个接口的更新频率是每5分钟一次。

这样的话,我完全可以定期获取这些热点新闻,主动放入内存中,预热 L1 本地缓存。这样一来,即使突发事件发生了,绝大多数请求都能直接命中 L1,完全绕过 Redis 和供应商 API。

本地缓存预热

5 分钟定时执行,将热点数据提前加载到本地缓存
1
调用供应商热点列表 API
获取最近的热点新闻 ID 列表(供应商每 5 分钟更新一次)
2
调用供应商获取新闻详情
遍历热点 ID,批量请求供应商 API 获取对应新闻内容
3
写入本地缓存
将新闻数据存入应用内存缓存,设置短 TTL(如 60 秒)
供应商 API
本地缓存
TTL 60s

当前架构