一、削峰填谷概述:什么是削峰填谷?为什么需要削峰填谷?核心思想与目标
1.1 从一个真实场景说起
我记得刚入行那会儿,接手过一个电商系统的订单处理模块。平时系统跑得挺稳,一到双十一就崩。不是代码写得烂,是流量瞬间冲上来,服务器扛不住。
后来我查了监控数据,发现一个规律:
- 平时每秒请求量:200-500
- 大促峰值每秒请求量:8000-12000
- 峰值持续时间:约30分钟
你想想看,为了这30分钟的峰值,去扩容10倍的服务器资源?老板肯定不答应。不扩容吧,系统直接雪崩。怎么办?
这就是典型的「削峰填谷」问题。
1.2 什么是削峰填谷?
说白了,削峰填谷就是把突发的流量高峰「削平」,把压力分散到更长的时间段里去处理。
我习惯用一个比喻来解释:
想象一下早高峰的地铁站
所有人同时涌进站台,站台会挤爆。但如果你在入口处设置闸机,控制放行速度,让乘客分批进入——这就是「削峰」。乘客虽然多,但站台始终不会超载。等过了早高峰,站台空了,再慢慢消化剩下的乘客——这就是「填谷」。
在系统设计中,削峰填谷的核心逻辑完全一样:
- 削峰:在流量入口处做限流或缓冲,不让瞬时流量直接冲击后端
- 填谷:把积压的请求在后续空闲时段慢慢处理掉
1.3 为什么需要削峰填谷?
这个问题我问过不少新人,答案五花八门。其实核心原因就三个:
| 原因 | 说明 | 我踩过的坑 |
|---|---|---|
| 成本控制 | 按峰值扩容,资源利用率极低。平时用不到,高峰期不够用 | 我曾经给一个项目配了8台机器应对秒杀,结果一年就用上两次,老板差点把我开了 |
| 系统稳定性 | 瞬时流量超过系统承载上限,会导致雪崩、OOM、连接池耗尽 | 有一次线上事故,就是数据库连接池被瞬间打满,整个服务挂了20分钟 |
| 用户体验 | 与其让用户看到「服务不可用」,不如让用户排队等待 | 嗯,这个道理我是在被用户骂了三次之后才真正理解的 |
1.4 核心思想与目标
削峰填谷的核心思想其实就一句话:用时间换空间。
什么意思?
- 空间:系统的并发处理能力、内存、连接数等资源
- 时间:请求的处理延迟、排队等待时间
我们牺牲一点响应时间,换取系统在高负载下的稳定运行。说白了,就是让用户多等几秒,但别让系统挂掉。
我的经验之谈
做削峰填谷设计时,我通常会问自己三个问题:
- 峰值流量到底有多大?—— 没有数据支撑的方案都是耍流氓
- 系统能容忍多大的延迟?—— 有些场景延迟1秒用户就跑了
- 积压的请求怎么保证不丢失?—— 这是最容易出坑的地方
1.5 削峰填谷的三种典型模式
在实际项目中,我常用的削峰填谷模式有三种:
| 模式 | 实现方式 | 适用场景 | 我踩过的坑 |
|---|---|---|---|
| 队列缓冲 | 请求先入队列,后端按能力消费 | 订单处理、消息推送 | 队列满了怎么办?我忘了做降级,结果内存爆了 |
| 令牌桶/漏桶 | 控制请求进入的速率 | API限流、网关层 | 令牌桶参数调不好,要么限太死,要么没效果 |
| 异步化处理 | 同步请求转为异步任务 | 报表生成、视频转码 | 异步回调超时没处理,用户以为订单丢了 |
1.6 知识体系总览
下面这张图是我自己总结的削峰填谷知识体系,你可以先有个整体印象:
1.7 一个简单的代码示例
光说不练假把式。我写个最简单的队列缓冲示例,帮你理解削峰填谷的代码长什么样:
// 伪代码:削峰填谷 - 队列缓冲模式
public class PeakShavingService {
// 阻塞队列,容量1000
private BlockingQueue<Request> queue = new LinkedBlockingQueue<>(1000);
// 消费者线程池,固定5个线程
private ExecutorService consumerPool = Executors.newFixedThreadPool(5);
// 请求入口 - 削峰
public boolean submit(Request req) {
// 队列满了就返回false,触发降级
boolean offered = queue.offer(req, 100, TimeUnit.MILLISECONDS);
if (!offered) {
// 我曾经在这里忘记记录日志,排查问题时一脸懵
log.warn("队列已满,请求被降级: {}", req.getId());
return false;
}
return true;
}
// 后台消费 - 填谷
@PostConstruct
public void startConsume() {
for (int i = 0; i < 5; i++) {
consumerPool.submit(() -> {
while (true) {
try {
Request req = queue.take(); // 没有请求就阻塞
processRequest(req); // 慢慢处理
} catch (Exception e) {
log.error("消费异常", e);
}
}
});
}
}
}
⚠️ 避坑指南
我曾经在队列满了之后直接抛异常,结果前端疯狂重试,把系统打得更惨。正确的做法是:队列满了就降级,返回一个「请稍后重试」的提示,同时记录日志方便排查。
1.8 本章小结
削峰填谷不是什么高深的技术,说白了就是「别让系统硬扛峰值流量」。核心就三点:
- 削峰:在入口处做缓冲或限流
- 填谷:在空闲时段消化积压
- 目标:用可接受的延迟,换取系统的稳定和低成本
后面的章节,我会带你深入每种模式的实现细节、参数调优、以及我在真实项目中踩过的坑。嗯,咱们慢慢来。