本文介绍在 Spring Boot 3 中实现多维度网络带宽限速的完整方案。基于令牌桶算法手动实现核心逻辑,通过自定义 HandlerInterceptor 拦截请求、HttpServletResponseWrapper 包装响应流、RateLimitedOutputStream 控制输出速率,实现对文件下载、视频流等场景的精确速度控制。

带宽限速与常见的 API 限流不同:限流控制的是请求次数(如每分钟100次),而限速控制的是网络带宽(如每秒200KB)。在实际应用中,带宽限速有着重要的业务价值:
对于网盘或资源分发平台,免费用户限制在 200KB/s,VIP 用户提升到 2MB/s,既能保障基础体验,又能激励付费转化。
不同清晰度对应不同带宽限制(480P 用 500KB/s,1080P 用 3MB/s),避免高码率视频占用过多服务器带宽。
大数据量接口(如导出报表)如果没有带宽控制,单个请求可能占满整个出口带宽,影响其他用户访问。
令牌桶算法是流量控制的经典方案,其思想非常直观:想象一个桶,系统以固定速率向桶中放入令牌,请求数据时必须从桶中取走对应数量的令牌。
发送数据前:
发送数据时:
本方案采用拦截器模式,在请求处理的早期阶段完成限速组件的初始化,通过请求属性传递包装后的响应对象。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
请求流程: ┌─────────────────────────────────────────────────────────────────────┐ │ 1. DispatcherServlet 分发请求 │ └─────────────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────────────┐ │ 2. BandwidthLimitInterceptor.preHandle() │ │ - 解析 @BandwidthLimit 注解 │ │ - 从 BandwidthLimitManager 获取共享 TokenBucket │ │ - 创建 BandwidthLimitResponseWrapper 并存入 request attribute │ └─────────────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────────────┐ │ 3. Controller 处理请求 │ │ - 通过 BandwidthLimitHelper.getLimitedResponse() 获取包装后的响应 │ │ - 向响应流写入数据(自动触发限速) │ └─────────────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────────────┐ │ 4. BandwidthLimitInterceptor.afterCompletion() │ │ - 清理资源,关闭流 │ └─────────────────────────────────────────────────────────────────────┘ |
在 Spring Boot 中实现请求处理,有两种常见方式:Filter 和 HandlerInterceptor。本方案选择 HandlerInterceptor 的关键原因是:注解解析需要 HandlerMethod 对象。
Filter 在 DispatcherServlet 之前执行,此时还没有确定具体的处理方法,无法获取方法上的 @BandwidthLimit 注解。而 HandlerInterceptor 在处理器确定后执行,可以通过 HandlerMethod 精确获取方法级别和类级别的注解信息。
|
组件 |
职责 |
|---|---|
|
@BandwidthLimit |
声明式注解,配置限速参数 |
|
BandwidthLimitInterceptor |
拦截请求,解析注解,创建响应包装器 |
|
BandwidthLimitManager |
管理多维度限速桶(全局/API/用户/IP) |
|
BandwidthLimitResponseWrapper |
包装 HttpServletResponse,替换 OutputStream |
|
RateLimitedOutputStream |
实现限速逻辑,包装 TokenBucket |
|
TokenBucket |
令牌桶算法实现 |
|
BandwidthLimitHelper |
从请求属性中获取包装后的响应对象 |
本方案支持四种限速维度,满足不同业务场景需求:
所有请求共享同一个限速桶,适合保护服务器整体出口带宽。例如设置 10MB/s 全局限制,即使有100个并发下载,总带宽也不会超过 10MB/s。
|
1 2 3 4 5 6 |
@BandwidthLimit(value = 200, unit = BandwidthUnit.KB, type = LimitType.GLOBAL) @GetMapping("/download/global") public void downloadGlobal(HttpServletResponse response) throws IOException { HttpServletResponse limitedResponse = BandwidthLimitHelper.getLimitedResponse(request, response); // 写入数据... } |
每个接口路径独立限速,不同接口的流量互不影响。/api/file/download 限制 500KB/s,/api/video/stream 限制 2MB/s,两个接口可以同时达到各自的速度上限。
|
1 2 3 4 5 6 7 8 9 10 11 |
@BandwidthLimit(value = 500, unit = BandwidthUnit.KB, type = LimitType.API) @GetMapping("/download/file") public void downloadFile(HttpServletResponse response) throws IOException { // 文件下载逻辑 }
@BandwidthLimit(value = 2048, unit = BandwidthUnit.KB, type = LimitType.API) @GetMapping("/stream/video") public void streamVideo(HttpServletResponse response) throws IOException { // 视频流逻辑 } |
根据用户标识(如请求头 X-User-Id)进行限速,每个用户独立计算带宽。配合 free 和 vip 参数,可实现差异化服务:
|
1 2 3 4 5 6 7 |
@BandwidthLimit(value = 200, unit = BandwidthUnit.KB, type = LimitType.USER, free = 200, vip = 2048) @GetMapping("/download/user") public void downloadByUser(@RequestHeader("X-User-Type") String userType, HttpServletResponse response) throws IOException { // 根据请求头 X-User-Type 自动应用 200KB/s 或 2MB/s 限速 } |
根据客户端 IP 地址限速,防止单个 IP 占用过多带宽。支持代理环境下的 IP 获取(X-Forwarded-For、X-Real-IP)。
|
1 2 3 4 5 |
@BandwidthLimit(value = 300, unit = BandwidthUnit.KB, type = LimitType.IP) @GetMapping("/download/ip") public void downloadByIp(HttpServletResponse response) throws IOException { // 每个独立 IP 限制 300KB/s } |
TokenBucket 的核心在于精确的时间计算和令牌补充。使用 System.nanoTime()获取纳秒级时间戳,确保高精度速率控制。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
public synchronized void acquire(long permits) { // 1. 补充令牌 refill();
// 2. 计算等待时间 if (tokens >= permits) { tokens -= permits; return; }
long deficit = permits - tokens; long waitNanos = (deficit * 1_000_000_000L) / refillRate;
// 3. 精确等待 sleepNanos(waitNanos);
// 4. 等待后消费 tokens = 0; }
private void refill() { long now = System.nanoTime(); long elapsedNanos = now - lastRefillTime; long newTokens = (elapsedNanos * refillRate) / 1_000_000_000L; tokens = Math.min(capacity, tokens + newTokens); lastRefillTime = now; } |
HttpServletResponseWrapper 是 Servlet 规范提供的响应包装基类,通过覆盖 getOutputStream() 方法返回自定义的限速输出流。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class BandwidthLimitResponseWrapper extends HttpServletResponseWrapper { privatefinal TokenBucket sharedTokenBucket; // 共享的令牌桶
@Override public ServletOutputStream getOutputStream() throws IOException { if (limitedOutputStream == null && sharedTokenBucket != null) { // 使用共享 TokenBucket,确保多维度统计正确 limitedOutputStream = new RateLimitedOutputStream( super.getOutputStream(), sharedTokenBucket, bandwidthBytesPerSecond ); } return limitedOutputStream; } } |
拦截器在 preHandle 中创建响应包装器,存储到 request attribute,Controller 通过 BandwidthLimitHelper 获取。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { BandwidthLimit annotation = findAnnotation(handler); if (annotation != null) { // 从 Manager 获取共享 TokenBucket TokenBucket bucket = limitManager.getBucket(type, key, capacity, rate);
// 创建包装器并存储 BandwidthLimitResponseWrapper wrappedResponse = new BandwidthLimitResponseWrapper(response, bucket, bandwidthBytesPerSecond, chunkSize); request.setAttribute("BandwidthLimitWrappedResponse", wrappedResponse); } return true; } |
Controller 通过 BandwidthLimitHelper.getLimitedResponse() 获取包装后的响应,所有写入操作都会自动限速。
|
1 2 3 4 5 6 7 8 9 10 |
@GetMapping("/download/global") public void downloadGlobal(HttpServletRequest request, HttpServletResponse response) throws IOException { HttpServletResponse limitedResponse = BandwidthLimitHelper.getLimitedResponse(request, response);
limitedResponse.setContentType("application/octet-stream"); limitedResponse.setHeader("Content-Disposition", "attachment; filename=test.bin");
// 写入数据时自动限速 limitedResponse.getOutputStream().write(data); } |
容量决定突发流量承受能力:
|
容量设置 |
突发能力 |
适用场景 |
|---|---|---|
|
速率 × 0.5 |
平滑,无突发 |
流量控制严格的场景 |
|
速率 × 1.0 |
允许 1 秒突发 |
默认推荐值 |
|
速率 × 2.0 |
允许 2 秒突发 |
需要良好首屏加载 |
|
1 2 |
// 注解配置 @BandwidthLimit(value = 200, unit = BandwidthUnit.KB, capacityMultiplier = 1.0) |
分块大小影响流量平滑度,经验公式:chunkSize = bandwidth / 50
|
带宽 |
推荐分块 |
理由 |
|---|---|---|
|
200 KB/s |
1-4 KB |
小分块保证平滑 |
|
1 MB/s |
4-8 KB |
平衡平滑与性能 |
|
5 MB/s+ |
8-16 KB |
减少系统调用开销 |
|
1 2 3 4 5 |
// 自动计算(推荐) @BandwidthLimit(value = 200, unit = BandwidthUnit.KB, chunkSize = -1)
// 手动指定 @BandwidthLimit(value = 200, unit = BandwidthUnit.KB, chunkSize = 4096) |
本文基于令牌桶算法,通过 HandlerInterceptor + HttpServletResponseWrapper,在 Spring Boot 中实现了多维度带宽限速。
支持全局/API/用户/IP 四种限速维度,提供实时统计监控,适用于API接口保护、文件下载、视频流等场景。