一、先明确:为什么考察 “削峰填谷”?
- 你是否能通过通俗场景理解 “削峰填谷” 的核心思想(不是死记定义)?
- 能否掌握 2-3 种主流实现方案(尤其是消息队列,高频考点),知道其原理与适用场景?
- 能否结合实际项目(如下单、秒杀)说明如何应用,体现实战能力?
- 能否识别方案的潜在风险(如队列堆积、消息丢失)及规避方法?
二、先铺垫:通俗认知
类比 1:洪水过闸
- 「峰值流量」:暴雨导致河流水位暴涨(对应系统突发高并发请求,如秒杀开始瞬间);
- 「削峰」:打开水库闸门,控制水流流速,避免洪水冲垮下游堤坝(对应限制请求进入系统的速率,避免系统过载);
- 「填谷」:枯水期时,水库放水补充河流流量(对应系统低峰期,消费队列中堆积的请求,避免资源闲置)。
类比 2:快递驿站
- 「峰值流量」:双十一快递爆仓,大量包裹同时到达驿站(对应系统突发高流量);
- 「削峰」:驿站将包裹统一存放,用户分时段取件(对应请求进入队列缓冲,不直接冲击业务系统);
- 「填谷」:平时快递量少,驿站快速处理剩余包裹(对应系统低峰期,匀速消费队列中的请求)。
简单说:削峰填谷是通过 “缓冲队列” 将突发的、不均匀的峰值流量,转化为平稳的、匀速的流量,避免系统因瞬时高负载崩溃,同时充分利用系统空闲资源,提升整体吞吐量。
三、核心定义:削峰填谷的官方解释与核心目标
1. 核心定义
削峰填谷是高并发系统中一种流量治理策略,通过引入中间缓冲层(如消息队列、Redis 队列),拦截突发的峰值请求,将其暂存起来,再按系统的处理能力匀速释放请求,最终实现 “峰值流量被削减、低谷期流量被填充” 的效果。
2. 核心目标(解决 3 大问题)
- 「防过载」:避免瞬时峰值流量超过系统最大处理能力(TPS/QPS),导致系统响应缓慢、超时甚至宕机;
- 「提效率」:充分利用系统低峰期的空闲资源,处理缓冲队列中的请求,避免资源浪费;
- 「稳系统」:使系统的流量输入平稳,减少因流量波动导致的服务抖动,提升系统稳定性。
四、核心应用场景
削峰填谷不是 “纸上谈兵”,在 Java 后端开发中随处可见,重点掌握以下场景:
| 场景类型 | 具体描述 | 示例场景 |
|---|---|---|
| 电商秒杀 / 促销活动 | 秒杀开始瞬间(如 0 点),大量用户同时下单,请求量突增(峰值是平时的 10 倍以上) | 淘宝双 11 秒杀、京东 618 促销下单 |
| 接口突发流量 | 某接口因被爬虫抓取、活动推广,短时间内收到大量请求(如 1 秒 1 万次请求) | 商品详情页接口被爬虫高频访问、APP 推送后用户集中打开 |
| 批量数据同步 / 导入 | 定时任务批量同步数据(如每天凌晨 3 点同步 10 万条订单数据),或用户批量导入 Excel(1 万条数据) | 订单数据同步到报表系统、用户批量导入商品信息 |
| 消息通知 / 日志收集 | 系统故障时,日志大量产生;或用户操作后,需发送大量短信 / 推送通知 | 系统异常日志上报、订单支付成功后发送短信通知 |
五、核心技术实现方案
方案 1:消息队列(最常用,必须掌握)—— 核心推荐
(1)核心原理
利用消息队列(如 RocketMQ、Kafka、RabbitMQ)的「队列缓冲特性」,将突发请求作为 “消息” 发送到队列中,业务系统作为 “消费者”,按自身处理能力匀速拉取消息并处理,实现削峰填谷。
- 「削峰」:峰值请求被队列拦截,不直接冲击业务系统,队列相当于 “流量缓冲池”;
- 「填谷」:低峰期时,队列中无新消息,业务系统继续消费剩余消息,充分利用空闲资源。
(2)关键保障
- 「消息可靠性」:开启消息持久化(避免队列宕机消息丢失)、重试机制(消费失败自动重试)、死信队列(重试多次失败的消息单独处理);
- 「消费速率控制」:通过消费者线程池大小、拉取批次(如每次拉取 100 条)控制消费速率,避免消费过快导致系统过载;
- 「队列容量限制」:设置队列最大长度(如 10 万条),避免消息堆积过多导致内存溢出。
(3)代码示例(基于 RocketMQ 实现秒杀下单削峰)
// 1. 生产者:秒杀下单请求发送到消息队列(削峰) @Service public class SeckillProducerService { @Autowired private RocketMQTemplate rocketMQTemplate; // 秒杀下单接口:接收用户下单请求,发送到队列 public String seckill(Long goodsId, Long userId) { try { // 1. 简单参数校验(如用户是否已秒杀过) if (checkSeckillStatus(goodsId, userId)) { return "您已参与过秒杀,请勿重复下单"; } // 2. 构造秒杀消息(将下单请求作为消息发送到队列) SeckillMessage message = new SeckillMessage(goodsId, userId, System.currentTimeMillis()); // 发送消息到队列(seckill_topic为队列主题) rocketMQTemplate.convertAndSend("seckill_topic", message); // 3. 直接返回“下单请求已接收”,无需等待业务处理完成(异步化) return "秒杀请求已提交,请稍后查询结果"; } catch (Exception e) { return "下单失败,请重试"; } } // 简单校验:用户是否已参与秒杀 private boolean checkSeckillStatus(Long goodsId, Long userId) { // 实际场景:查询Redis/数据库,判断用户是否已下单 return false; } } // 2. 消费者:按处理能力匀速消费消息(填谷) @Service @RocketMQMessageListener( topic = "seckill_topic", // 监听的队列主题 consumerGroup = "seckill_consumer_group", // 消费者组 consumeThreadMax = 20, // 最大消费线程数(控制消费速率) consumeMessageBatchMaxSize = 50 // 每次拉取最大消息数 ) public class SeckillConsumerService implements RocketMQListener<SeckillMessage> { @Autowired private SeckillService seckillService; @Override public void onMessage(SeckillMessage message) { try { // 消费消息:执行实际的秒杀下单业务(扣减库存、创建订单) seckillService.doSeckill(message.getGoodsId(), message.getUserId()); } catch (Exception e) { // 消费失败:消息队列会自动重试(默认重试16次) log.error("秒杀下单消费失败,goodsId={}, userId={}", message.getGoodsId(), message.getUserId(), e); // 若重试多次失败,消息会进入死信队列,后续人工处理 throw new RuntimeException("消费失败,触发重试"); } } }(4)优缺点与适用场景
| 特性 | 详情描述 |
|---|---|
| 优点 | 1. 削峰效果好:支持高并发消息堆积(如 Kafka 支持百万级消息 / 秒);2. 异步化:生产者发送消息后立即返回,提升用户体验;3. 高可用:支持集群部署,避免单点故障;4. 功能完善:自带重试、死信队列等特性,无需手动实现 |
| 缺点 | 1. 引入中间件:需部署维护消息队列,增加系统复杂度;2. 消息堆积风险:若消费速率长期低于生产速率,消息会堆积,需监控告警;3. 一致性问题:异步处理可能导致用户 “下单成功但查询不到订单”(需通过状态查询接口解决) |
| 适用场景 | 高并发、峰值流量突出的场景(如秒杀、促销、接口突发流量),是实习生必须掌握的方案 |
方案 2:请求排队(基于 Redis/ZooKeeper)—— 轻量级方案
(1)核心原理
利用 Redis 的「List/Set 数据结构」或 ZooKeeper 的「有序节点」,将请求参数存储在队列中,业务系统通过定时任务(如每 100 毫秒执行一次)或线程池,从队列中取出请求并处理,控制处理速率。
- 「削峰」:请求存入 Redis 队列,不直接调用业务接口;
- 「填谷」:低峰期时,队列中请求减少,定时任务继续处理剩余请求。
(2)代码示例(基于 Redis List 实现批量数据导入削峰)
@Service public class BatchImportService { @Autowired private StringRedisTemplate redisTemplate; @Autowired private GoodsMapper goodsMapper; // 1. 接收批量导入请求,存入Redis队列(削峰) public String batchImportGoods(List<Goods> goodsList) { try { // 将商品数据序列化为JSON,存入Redis List队列(key=goods_import_queue) for (Goods goods : goodsList) { String goodsJson = JSON.toJSONString(goods); redisTemplate.opsForList().leftPush("goods_import_queue", goodsJson); } return "导入请求已接收,共" + goodsList.size() + "条数据,正在处理中"; } catch (Exception e) { return "导入失败,请重试"; } } // 2. 定时任务:每100毫秒从队列中取出数据处理(填谷) @Scheduled(fixedRate = 100) // 100毫秒执行一次 public void processImportQueue() { try { // 每次从队列尾部取出10条数据(控制处理速率) List<String> goodsJsonList = redisTemplate.opsForList().rightPop("goods_import_queue", 10); if (CollectionUtils.isEmpty(goodsJsonList)) { return; // 队列无数据,直接返回 } // 批量插入数据库(填谷:低峰期时,队列数据被匀速处理) List<Goods> goodsList = goodsJsonList.stream() .map(json -> JSON.parseObject(json, Goods.class)) .collect(Collectors.toList()); goodsMapper.batchInsert(goodsList); } catch (Exception e) { log.error("处理导入队列失败", e); } } }(3)优缺点与适用场景
| 特性 | 详情描述 |
|---|---|
| 优点 | 1. 轻量级:无需部署独立消息队列,依赖 Redis(大多数项目已集成);2. 实现简单:仅需 Redis List/ZSet 操作,开发成本低;3. 无额外依赖:适合中小型系统 |
| 缺点 | 1. 功能简陋:无自带重试、死信队列,需手动实现;2. 堆积风险:Redis List 无最大长度限制(需手动控制),消息过多可能导致 Redis 内存溢出;3. 不支持高并发:Redis 的 QPS 有限(单节点约 10 万 QPS),无法应对超大规模峰值 |
| 适用场景 | 中小型系统、低并发场景(如批量数据导入、内部系统接口削峰),不适合秒杀等高并发场景 |
方案 3:限流 + 降级(配合削峰,辅助方案)
(1)核心原理
限流(如 Sentinel、Guava RateLimiter)限制单位时间内的请求量(如 1 秒 5000 次请求),超过限制的请求直接降级处理(如返回 “系统繁忙,请稍后重试”),避免峰值流量冲击系统;同时结合缓冲队列,实现 “限流 + 削峰” 双重保障。
- 「削峰」:限流直接拦截超阈值的峰值请求,避免进入系统;
- 「填谷」:未被限流的请求进入队列,匀速处理。
(2)代码示例(基于 Sentinel+RocketMQ 实现限流削峰)
@Service public class SeckillService { @Autowired private RocketMQTemplate rocketMQTemplate; // 1. 秒杀接口:添加Sentinel限流(1秒最多5000次请求) @SentinelResource( value = "seckillInterface", blockHandler = "seckillBlockHandler" // 限流降级处理方法 ) public String seckill(Long goodsId, Long userId) { // 限流通过的请求,发送到消息队列 SeckillMessage message = new SeckillMessage(goodsId, userId); rocketMQTemplate.convertAndSend("seckill_topic", message); return "秒杀请求已提交,请稍后查询"; } // 2. 限流降级处理:超过QPS阈值的请求,直接返回提示 public String seckillBlockHandler(Long goodsId, Long userId, BlockException e) { log.warn("秒杀接口限流,goodsId={}, userId={}", goodsId, userId); return "系统繁忙,请稍后重试"; } } // Sentinel配置(application.yml) spring: cloud: sentinel: transport: dashboard: localhost:8080 # Sentinel控制台地址 datasource: ds1: flow: rule: resource: seckillInterface # 限流资源名 limitApp: default grade: 1 # 1=QPS限流 count: 5000 # 每秒最多5000次请求(3)优缺点与适用场景
| 特性 | 详情描述 |
|---|---|
| 优点 | 1. 快速拦截:直接在网关 / 接口层拦截超阈值请求,无缓冲开销;2. 保护系统:避免系统被峰值流量压垮;3. 配合性强:可与消息队列结合,实现 “限流拦截 + 队列缓冲” 双重保障 |
| 缺点 | 1. 用户体验差:限流降级的请求直接被拒绝,用户需重试;2. 无填谷能力:仅能削峰,无法利用低峰期资源处理请求 |
| 适用场景 | 高并发场景的 “第一道防线”(如秒杀接口、公开接口),需与消息队列配合使用,提升用户体验 |
方案 4:异步化处理(本质是 “请求与处理解耦”)
(1)核心原理
将同步执行的业务操作改为异步执行(如通过线程池、Spring @Async 注解),请求发起者无需等待处理结果,直接返回,业务操作在后台异步执行,从而缓解峰值压力。
- 「削峰」:同步请求改为异步后,接口响应时间大幅缩短(如从 1 秒变为 10 毫秒),系统能处理更多并发请求;
- 「填谷」:异步任务在后台匀速执行,避免瞬时处理压力。
(2)代码示例(基于 Spring @Async 实现异步通知削峰)
@Service @EnableAsync // 开启异步支持 public class NoticeService { // 1. 异步发送短信通知(后台线程执行,不阻塞主线程) @Async("noticeThreadPool") // 指定线程池 public CompletableFuture<Void> sendSms(String phone, String content) { try { // 模拟发送短信(调用短信API) smsApi.send(phone, content); log.info("短信发送成功,phone={}", phone); } catch (Exception e) { log.error("短信发送失败,phone={}", phone, e); } return CompletableFuture.runAsync(() -> {}); } // 2. 配置异步线程池(控制并发数,避免线程过多) @Bean(name = "noticeThreadPool") public Executor noticeThreadPool() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10); // 核心线程数 executor.setMaxPoolSize(20); // 最大线程数 executor.setQueueCapacity(1000); // 任务队列容量 executor.setThreadNamePrefix("sms-notice-"); executor.initialize(); return executor; } } // 调用方:订单支付成功后发送短信,异步执行(削峰) @Service public class OrderService { @Autowired private NoticeService noticeService; public void paySuccess(Order order) { // 1. 执行本地事务(创建订单、扣减库存) updateOrderStatus(order.getId(), "PAID"); // 2. 异步发送短信(不阻塞主线程,快速响应) noticeService.sendSms(order.getPhone(), "您的订单" + order.getOrderNo() + "已支付成功"); } }(3)优缺点与适用场景
| 特性 | 详情描述 |
|---|---|
| 优点 | 1. 响应快:接口快速返回,提升用户体验;2. 解耦:请求与处理分离,系统扩展性好;3. 实现简单:基于 Spring @Async 或线程池,开发成本低 |
| 缺点 | 1. 无缓冲队列:若异步任务过多,线程池队列会堆积,可能导致内存溢出;2. 无重试机制:任务执行失败需手动实现重试;3. 一致性风险:异步处理可能导致数据不一致(如订单支付成功但短信未发送) |
| 适用场景 | 非核心业务异步化(如消息通知、日志记录),需配合重试机制使用,避免任务丢失 |
加分项
- 结合项目举例:“我在实训项目的秒杀模块中,用 RocketMQ 实现削峰填谷,秒杀请求发送到队列后,消费者按每秒 500 次的速率处理,避免了系统过载;同时用 Sentinel 限流,每秒最多允许 5000 次请求,超过的请求返回‘系统繁忙’,双重保障高并发稳定性”;
- 考虑消息可靠性:“使用消息队列时,我会开启持久化和重试机制,配置死信队列处理失败消息;还会设置队列最大长度,避免消息堆积过多导致 OOM”;
- 区分场景选型:“秒杀等高并发场景用 RocketMQ,中小型系统的批量导入用 Redis 队列,非核心业务的异步通知用 Spring @Async”;
- 监控告警意识:“我会给队列添加监控,当消息堆积超过阈值(如 5000 条)时触发告警,及时扩容消费者或排查处理瓶颈”。
踩坑点
- 只削峰不填谷:仅用限流拦截请求,未配合缓冲队列,导致大量请求被拒绝,用户体验差;
- 忽略消息丢失:使用消息队列时未开启持久化,队列宕机后消息丢失,导致业务数据不一致;
- 无队列容量限制:Redis 队列或消息队列未设置最大长度,峰值流量过大时,消息堆积导致内存溢出;
- 消费速率失控:消费者拉取消息过快(如每次拉取 1 万条),导致业务系统过载,反而引发故障;
- 异步化滥用:核心业务(如下单、支付)用异步化处理,未考虑数据一致性和重试机制,导致业务失败。
举一反三
- “消息队列实现削峰填谷时,如何避免消息堆积?”(答案:① 监控队列堆积量,超过阈值触发告警;② 动态扩容消费者(增加线程数、部署更多消费者节点);③ 优化业务处理逻辑,提升单条消息处理速度;④ 非核心消息可降级丢弃,优先处理核心消息);
- “削峰填谷和限流的区别是什么?为什么要配合使用?”(答案:① 削峰填谷是 “缓冲 + 匀速处理”,不拒绝请求,仅延迟处理;限流是 “直接拦截超阈值请求”,拒绝部分请求;② 配合使用:限流拦截极端峰值,避免队列溢出;队列缓冲正常峰值,提升用户体验,双重保障系统稳定);
- “Redis 队列和消息队列(如 RocketMQ)实现削峰填谷的区别是什么?”(答案:① Redis 队列:轻量级、无额外依赖,但功能简陋(无重试、死信队列),适合低并发;② 消息队列:功能完善、支持高并发,但需独立部署维护,适合高并发场景(如秒杀));
- “如果没有消息队列和 Redis,如何实现简单的削峰填谷?”(答案:① 用 Java 的 BlockingQueue 作为本地队列,缓冲请求;② 定时任务匀速处理队列中的请求;③ 限制队列容量,避免内存溢出,适合单节点、低并发场景);
- “秒杀场景中,消息队列的消息重复消费问题如何解决?”(答案:① 消费端实现幂等性(如通过订单号 + 用户 ID 去重);② 数据库层面添加唯一索引(如订单表的 order_no 唯一);③ 消息队列中给每条消息分配唯一 ID,消费端记录已消费的消息 ID,避免重复处理)。