一、削峰填谷概述:什么是削峰填谷?为什么需要削峰填谷?核心思想与目标

1.1 从一个真实场景说起

我记得刚入行那会儿,接手过一个电商系统的订单处理模块。平时系统跑得挺稳,一到双十一就崩。不是代码写得烂,是流量瞬间冲上来,服务器扛不住。

后来我查了监控数据,发现一个规律:

  • 平时每秒请求量:200-500
  • 大促峰值每秒请求量:8000-12000
  • 峰值持续时间:约30分钟

你想想看,为了这30分钟的峰值,去扩容10倍的服务器资源?老板肯定不答应。不扩容吧,系统直接雪崩。怎么办?

这就是典型的「削峰填谷」问题。

1.2 什么是削峰填谷?

说白了,削峰填谷就是把突发的流量高峰「削平」,把压力分散到更长的时间段里去处理。

我习惯用一个比喻来解释:

想象一下早高峰的地铁站

所有人同时涌进站台,站台会挤爆。但如果你在入口处设置闸机,控制放行速度,让乘客分批进入——这就是「削峰」。乘客虽然多,但站台始终不会超载。等过了早高峰,站台空了,再慢慢消化剩下的乘客——这就是「填谷」。

在系统设计中,削峰填谷的核心逻辑完全一样:

  • 削峰:在流量入口处做限流或缓冲,不让瞬时流量直接冲击后端
  • 填谷:把积压的请求在后续空闲时段慢慢处理掉

1.3 为什么需要削峰填谷?

这个问题我问过不少新人,答案五花八门。其实核心原因就三个:

原因 说明 我踩过的坑
成本控制 按峰值扩容,资源利用率极低。平时用不到,高峰期不够用 我曾经给一个项目配了8台机器应对秒杀,结果一年就用上两次,老板差点把我开了
系统稳定性 瞬时流量超过系统承载上限,会导致雪崩、OOM、连接池耗尽 有一次线上事故,就是数据库连接池被瞬间打满,整个服务挂了20分钟
用户体验 与其让用户看到「服务不可用」,不如让用户排队等待 嗯,这个道理我是在被用户骂了三次之后才真正理解的

1.4 核心思想与目标

削峰填谷的核心思想其实就一句话:用时间换空间

什么意思?

  • 空间:系统的并发处理能力、内存、连接数等资源
  • 时间:请求的处理延迟、排队等待时间

我们牺牲一点响应时间,换取系统在高负载下的稳定运行。说白了,就是让用户多等几秒,但别让系统挂掉。

我的经验之谈

做削峰填谷设计时,我通常会问自己三个问题:

  1. 峰值流量到底有多大?—— 没有数据支撑的方案都是耍流氓
  2. 系统能容忍多大的延迟?—— 有些场景延迟1秒用户就跑了
  3. 积压的请求怎么保证不丢失?—— 这是最容易出坑的地方

1.5 削峰填谷的三种典型模式

在实际项目中,我常用的削峰填谷模式有三种:

模式 实现方式 适用场景 我踩过的坑
队列缓冲 请求先入队列,后端按能力消费 订单处理、消息推送 队列满了怎么办?我忘了做降级,结果内存爆了
令牌桶/漏桶 控制请求进入的速率 API限流、网关层 令牌桶参数调不好,要么限太死,要么没效果
异步化处理 同步请求转为异步任务 报表生成、视频转码 异步回调超时没处理,用户以为订单丢了

1.6 知识体系总览

下面这张图是我自己总结的削峰填谷知识体系,你可以先有个整体印象:

削峰填谷知识体系 核心思想:用时间换空间 三大目标:成本 · 稳定 · 体验 队列缓冲 请求排队,按能力消费 令牌桶/漏桶 控制请求进入速率 异步化处理 同步转异步任务 消息队列(RabbitMQ/Kafka) 限流组件(Guava/Sentinel) 线程池/协程 常见坑点:队列溢出 · 限流参数不合理 · 异步回调丢失 · 降级策略缺失

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 本章小结

削峰填谷不是什么高深的技术,说白了就是「别让系统硬扛峰值流量」。核心就三点:

  • 削峰:在入口处做缓冲或限流
  • 填谷:在空闲时段消化积压
  • 目标:用可接受的延迟,换取系统的稳定和低成本

后面的章节,我会带你深入每种模式的实现细节、参数调优、以及我在真实项目中踩过的坑。嗯,咱们慢慢来。


专注资料整理