流量洪峰:热门短链接带来的流量冲击
想象一下这个场景:
-
某明星突然宣布结婚 📱
- 粉丝疯狂转发相关链接
- 一小时内点击量从 1000 爆发到 1000万+
-
重大新闻发布 📰
- 权威媒体报道重要新闻
- 相关短链接在社交媒体病毒式传播
-
限时优惠活动 🎁
- 网络红人发布限时折扣
- 短链接瞬间被几十万人同时点击
-
热门话题讨论 💬
- 热点话题引发热议
- 相关讨论和分享激增
真实案例:
- 某电商平台黑五活动期间,短链接点击量单日突破 1 亿次
- 某新闻客户端热门新闻的短链接,3 分钟内被访问 500 万次
- 某娱乐明星绯闻相关链接,2 小时内访问量破千万
当流量突然暴增时,我们的短链接系统会面临什么?
现状问题分析
让我们先看看我们的”完美”系统是如何设计的:
落地思路
- 这个「完美」系统的跳转链路只有一步:拿短码查
urls表,靠idx_short_code定位到那一条记录,取出目标长 URL 直接返回 302。- 每一次点击都对应一次真实的数据库查询,前面没有任何缓存或旁路能挡掉这次查询。
- 应用实例直接连 MySQL,连接池是链路上唯一的闸门——请求量涨上来时它最先被占满。
这个简单系统在流量洪峰面前的表现:
落地思路
- 洪峰到来时,连接池里的连接被排队占满,新请求拿不到连接就只能等,超时后报 “too many connections”。
- 排在后面的请求要等前面的慢查询做完,响应时间被拉长到秒级,用户点击后长时间无反应。
- 处理速度追不上到达速度时,请求在服务内部持续堆积,内存占用一路上涨,最后被 OOM Killer 直接干掉。
- 整条链路没有任何限流点:后端已经被压垮了,入口层还在源源不断地把请求送进来。
具体故障表现
| 故障类型 | 具体表现 | 用户感受 |
|---|---|---|
| 数据库连接池耗尽 | “too many connections” 错误 | 页面无法打开 |
| 响应时间过长 | 每次请求等待 5-10 秒 | 点击后长时间无反应 |
| 请求队列堆积 | 请求积压在队列中 | 点击后数分钟才跳转 |
| 系统内存溢出 | OOM Killer 杀死进程 | 服务完全不可用 |
策略 1:缓存层防护 🛡️
首先,我们需要在最前面加上缓存层:
落地思路
- 在跳转链路最前面插一层 Redis,存「短码 → 目标长 URL」的映射,命中缓存就直接返回 302,不碰数据库。
- 缓存是只读旁路:缓存里没有也不影响主流程,查不到就回落到数据库查询再回填。
- 这一层把绝大多数请求挡在 MySQL 之前,数据库只处理缓存未命中的那部分流量。
- 兜底是 Redis 不可用时降级成直查数据库,而不是让整个跳转功能直接不可用。
缓存策略配置:
落地思路
- 键就用短码本身,不加额外前缀,值存目标长 URL,TTL 按热点时长来定。
- 写入顺序是「先更新数据库,再删缓存」:直接改缓存里的值会和数据库产生不一致。
- 热点短码的 TTL 要比普通链接长,让官宣、抽奖这类短链接在洪峰期一直留在缓存里。
- 兜底是给「查不到」的短码也缓存一个短 TTL 的空标记,防止不存在的短码被反复打到数据库上。
策略 2:负载均衡 ⚖️
单个服务器扛不住?那就用多个!
落地思路
- 在应用前面放一组反向代理(Nginx 或云负载均衡),把请求按连接数或轮询分给多个应用实例。
- 代理侧限制每个上游的并发连接数,避免某一个实例被慢请求占满把整组拖下水。
- 代理必须透传
X-Forwarded-For和X-Real-IP,否则后端拿到的全是代理 IP,限流和访问日志都会失真。- 某个实例挂掉时,代理把它摘掉、流量转给健康实例,用户感知到的是偶发失败而不是整站不可用。
策略 3:流量削峰 📊
防患于未然,在流量达到峰值前就采取措施:
落地思路
- 点击事件不直接同步写 MySQL:入口只做「短码 → 长 URL」的解析,把点击记录投进消息队列,由消费者批量落库。
- 队列把「瞬时到达的点击」和「数据库能消化的写入速度」解耦,突发流量先在队列里排队。
- 队列积压超过阈值时先告警、再降速消费,宁可让统计数据延迟,也不能把数据库写垮。
- 跳转本身不依赖这条链路,所以队列积压只影响统计口径,不影响用户能不能点开链接。
策略 4:降级机制 🔧
系统扛不住时,优雅降级:
落地思路
- 数据库不可用时降级成「只读缓存」:缓存里有就直接 302,没有就返回明确的服务异常页,而不是让请求长时间挂起。
- 给数据库调用设置超时上限,超过就快速失败,把慢查询占住的连接尽快还给连接池。
- 熔断器统计下游失败比例,超过阈值就打开、直接跳过真实调用,快速失败比排队等超时更友好。
- 降级期间只保留核心的「短码 → 长 URL」跳转,统计上报这类非核心能力先关掉。
让我们对比不同策略的效果:
落地思路
- 对比方式是同一台压测机上逐级叠加策略:裸查数据库 → 加缓存 → 加负载均衡 → 加队列削峰,每一级记录吞吐和响应时间。
- 关注三组数据:缓存命中率、平均响应时间和错误率,跟本章小结里的目标值(命中率 > 80%、响应 < 100ms、错误率 < 1%)对齐。
- 洪峰用突增流量模拟,验证的是系统从峰值回落后能不能自动恢复,而不只是峰值那一瞬间的极限吞吐。
- 结论看趋势:缓存层对数据库负载的削减最直接,削峰和降级决定的是系统会不会崩。
Redis 缓存配置
验证要点
- 命中率用
INFO stats里的keyspace_hits和keyspace_misses相除得到,这是判断缓存层有没有生效的第一手数据。- 再用
TTL <短码>确认热点短码的剩余有效期没有比预期更早归零——TTL 设短了,命中率会莫名其妙往下掉。- 抽查一个短码的缓存值(
GET <短码>),确认里面存的是目标长 URL 而不是整个 302 响应,否则回源时拿到的不是想要的东西。- 期望看到的命中率达到本章小结里的 80% 目标;低于这个数,先查 TTL 和键的过期策略。
入口层限流策略
入口配置要点
- 入口层负责接住流量,并把请求转发到后端服务。
- 限流和连接控制放在入口层,可以更早保护后端。
- 代理配置的重点是保留必要请求信息,方便后端识别来源和生成日志。
面对流量洪峰,我们的防御体系包括:
| 防御层 | 作用 | 效果 |
|---|---|---|
| 缓存层 | 减少数据库查询 | 80%+ 命中率,降低 80% 数据库负载 |
| 负载均衡 | 分散请求压力 | 线性扩展,支持更多并发 |
| 流量削峰 | 防止系统过载 | 优雅降级,避免崩溃 |
| 熔断机制 | 快速故障转移 | 快速恢复,减少影响范围 |
关键指标
| 指标 | 目标值 | 监控方法 |
|---|---|---|
| 缓存命中率 | > 80% | Redis INFO |
| 平均响应时间 | < 100ms | 应用监控 |
| 错误率 | < 1% | 日志分析 |
| 服务器负载 | < 70% | 系统监控 |
设计原则
- 缓存优先:尽量使用缓存,减少数据库访问
- 降级处理:宁可降级也不崩溃
- 监控预警:及时发现异常,主动处理
- 弹性伸缩:根据流量动态扩缩容
我们解决了流量洪峰问题,但还有一个关键挑战:如何在分布式环境中生成全局唯一的短链接 ID?
答案:分布式 ID 生成器。我们将在下一章深入探讨。
练习 1
如果缓存命中率只有 60%,流量从 1000 QPS 增加到 10000 QPS,数据库负载增加了多少?
解题思路:
这题问的是”负载增加了多少”,所以不能用总量算,必须算增量:两次流量下的数据库查询数相减。关键是缓存命中率只对未命中的那 40% 请求起作用,命中那 60% 根本不落到数据库。
详细步骤:
计算过程:
-
无缓存时的数据库负载
- 1000 QPS:1000 次数据库查询/秒
- 10000 QPS:10000 次数据库查询/秒
- 增长:10000 - 1000 = 9000 次/秒
-
60% 缓存命中率时的数据库负载
- 1000 QPS:1000 × (1 - 0.6) = 400 次数据库查询/秒
- 10000 QPS:10000 × (1 - 0.6) = 4000 次数据库查询/秒
- 增长:4000 - 400 = 3600 次/秒
-
负载对比
- 无缓存:数据库负载增长 9000 次/秒
- 有缓存:数据库负载增长 3600 次/秒
- 减少:9000 - 3600 = 5400 次/秒(减少 60%)
最终答案:
即使只有 60% 的缓存命中率,数据库负载仍然减少了 60%。从 9000 次增长减少到 3600 次增长。
练习 2
熔断器机制的原理是什么?为什么要在系统中实现熔断?
解题思路:
熔断器要解决的不是”下游会不会挂”,而是”下游挂了之后我这边也被拖死”这个问题。所以要理解它其实是个状态机,围绕关闭、打开、半开三态的切换条件去记,而不是只记它的存在。
详细步骤:
熔断器原理:
熔断器模式类似于家庭电路中的保险丝,当电路过载时自动切断电路,保护设备和安全。
工作机制:
- 关闭状态(Closed):正常调用,统计失败次数
- 打开状态(Open):直接返回降级结果,不执行真实调用
- 半开状态(Half-Open):允许少量请求尝试恢复
为什么要实现熔断:
-
防止级联故障🔥
- 当下游服务故障时,防止大量请求堆积
- 避免故障向整个系统扩散
-
快速恢复⚡
- 避免等待超时(可能 30-60 秒)
- 立即降级,用户体验更好
-
资源保护💪
- 避免浪费资源在已故障的服务上
- 让健康的服务能正常处理请求
-
故障隔离🚧
- 将故障服务隔离,不影响其他功能
- 便于运维人员快速定位问题
实际案例:
- 微服务调用中,A 服务调用 B 服务
- B 服务故障时,大量请求超时
- 使用熔断器后,直接返回缓存数据
- A 服务仍然可以正常工作,用户体验不受影响
最终答案:
熔断器类似家庭电路的保险丝,下游过载时自动切断调用以保护自身。它有三个状态:关闭状态正常调用并统计失败次数;打开状态直接返回降级结果、不再发起真实调用;半开状态放少量请求试探下游是否恢复。之所以要实现熔断,是因为它能防止级联故障(下游故障时不让请求堆积扩散)、实现快速恢复(不等 30 到 60 秒超时而是立即降级)、保护资源(不把资源浪费在已故障的服务上)、隔离故障(便于运维定位)。典型场景是 A 服务调用 B 服务,B 故障时大量请求超时,加了熔断后 A 直接返回缓存数据,用户体验不受影响。
练习 3
设计一个滑动窗口限流算法,如何实现?
解题思路:
限流算法要对比着固定窗口来理解:固定窗口的毛病是边界处会放行双倍流量,而滑动窗口用”最近 N 秒”这个连续区间来计数,天然没有这个漏洞。实现上要抓住”窗口状态存在哪里、怎么过期”两个点。
详细步骤:
滑动窗口限流设计:
落地思路
- 滑动窗口把时间切成一格一格的小窗口,每个窗口维护一个计数,限流判断时把当前窗口和前面若干个窗口的计数相加。
- 相加覆盖的范围就是「最近 N 秒」,所以计数既能反映瞬时峰值,也不会像固定窗口那样在边界处放行双倍流量。
- 在 Redis 里可以用有序集合实现:每个请求记一个带时间戳的成员,统计前先把超出窗口范围的成员删掉再计数。
- 兜底是给窗口状态设置过期时间:窗口内没有任何请求就自动清掉,不留下永久增长的键。
滑动窗口 vs 固定窗口:
| 特性 | 滑动窗口 | 固定窗口 |
|---|---|---|
| 精度 | 精确(1秒内) | 粗略(60秒内) |
| 实现 | 较复杂(需要维护时间戳) | 简单(计数器) |
| 性能 | 稍低(时间戳操作) | 较高(简单计数) |
| 公平性 | 更公平 | 不公平(边界问题) |
最终答案:
滑动窗口把时间切成一格一格的小窗口,每个窗口维护一个计数,限流判断时把当前窗口和前面若干个窗口的计数相加,覆盖范围正好是最近 N 秒,既能反映瞬时峰值,又不会像固定窗口那样在边界放行双倍流量。在 Redis 里可用有序集合实现:每个请求记一个带时间戳的成员,统计前先删掉超出窗口范围的成员再计数,并给窗口状态设置过期时间,空窗口自动清理。相比固定窗口,滑动窗口精度更高(1 秒内而不是 60 秒内)、更公平,但实现较复杂、性能稍低,需要额外维护时间戳。
发现内容有问题?
告诉我们哪里需要改进。
