- ✅ 使用 Redis 记录 所有用户的实时并发下载数
- ✅ 使用 Bucket4j 实现 全局下载速率限制(动态)
- ✅ 支持 动态调整限速策略
- ✅ 下载接口安全、稳定、可监控
🧩 整体架构概览
模块 | 功能 |
---|---|
Redis | 存储全局并发数和带宽令牌桶状态 |
Bucket4j + Redis | 分布式限速器(基于令牌桶算法) |
Spring Boot Web | 提供文件下载接口 |
AOP / Interceptor(可选) | 用于统一处理限流逻辑 |
📦 1. Maven 依赖(pom.xml
)
xml
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Redis 连接池 -->
<dependency>
<groupId>io.lettuce.core</groupId>
<artifactId>lettuce-core</artifactId>
</dependency>
<!-- Bucket4j 核心与 Redis 集成 -->
<dependency>
<groupId>com.github.vladimir-bukhtoyarov</groupId>
<artifactId>bucket4j-core</artifactId>
<version>5.3.0</version>
</dependency>
<dependency>
<groupId>com.github.vladimir-bukhtoyarov</groupId>
<artifactId>bucket4j-redis</artifactId>
<version>5.3.0</version>
</dependency>
</dependencies>
🛠️ 2. Redis 工具类:记录全局并发数
java
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
@Component
public class GlobalDownloadCounter {
private final StringRedisTemplate redisTemplate;
private final DefaultRedisScript<Long> incrScript;
private final DefaultRedisScript<Long> decrScript;
public static final String KEY_CONCURRENT = "global:download:concurrent";
private static final long TTL_SECONDS = 60; // 自动清理僵尸计数
public GlobalDownloadCounter(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
// Lua 脚本:原子增加并发数并设置过期时间
String scriptIncr = """
local key = KEYS[1]
local ttl = tonumber(ARGV[1])
local count = redis.call('GET', key)
if not count then
redis.call('SET', key, 1)
redis.call('EXPIRE', key, ttl)
return 1
else
count = tonumber(count) + 1
redis.call('SET', key, count)
redis.call('EXPIRE', key, ttl)
return count
end
""";
incrScript = new DefaultRedisScript<>(scriptIncr, Long.class);
// Lua 脚本:原子减少并发数
String scriptDecr = """
local key = KEYS[1]
local count = redis.call('GET', key)
if not count or tonumber(count) <= 0 then
return 0
else
count = tonumber(count) - 1
redis.call('SET', key, count)
return count
end
""";
decrScript = new DefaultRedisScript<>(scriptDecr, Long.class);
}
public long increment() {
return redisTemplate.execute(incrScript, Collections.singletonList(KEY_CONCURRENT), TTL_SECONDS).longValue();
}
public long decrement() {
return redisTemplate.execute(decrScript, Collections.singletonList(KEY_CONCURRENT)).longValue();
}
public long getCurrentCount() {
String value = redisTemplate.opsForValue().get(KEY_CONCURRENT);
return value == null ? 0 : Long.parseLong(value);
}
}
⚙️ 3. Bucket4j 配置:分布式限速器(带 Redis)
java
import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Refill;
import io.github.bucket4j.distributed.proxy.ProxyManager;
import io.github.bucket4j.distributed.proxy.RedisProxyManager;
import io.github.bucket4j.redis.lettuce.cas.LettuceReactiveProxyManager;
import io.lettuce.core.RedisClient;
import io.lettuce.core.api.StatefulRedisConnection;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;
@Configuration
public class BandwidthLimiterConfig {
@Bean
public RedisClient redisClient() {
return RedisClient.create("redis://localhost:6379");
}
@Bean
public StatefulRedisConnection<String, String> redisConnection(RedisClient redisClient) {
return redisClient.connect();
}
@Bean
public ProxyManager<String> proxyManager(StatefulRedisConnection<String, String> connection) {
return LettuceReactiveProxyManager.builder()
.build(connection.reactive());
}
@Bean
public Bandwidth globalBandwidthLimit() {
// 默认 10MB/s
return Bandwidth.classic(10 * 1024 * 1024, Refill.greedy(10 * 1024 * 1024, Duration.ofSeconds(1)));
}
@Bean
public Bucket globalBandwidthLimiter(ProxyManager<String> proxyManager, Bandwidth bandwidthLimit) {
return proxyManager.builder().build("global:bandwidth:limiter", bandwidthLimit);
}
}
📡 4. 下载接口实现
java
import io.github.bucket4j.Bucket;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;
@RestController
@RequestMapping("/api/download")
public class DownloadController {
private static final int MAX_CONCURRENT_DOWNLOADS = 100;
private final GlobalDownloadCounter downloadCounter;
private final Bucket bandwidthLimiter;
public DownloadController(GlobalDownloadCounter downloadCounter, Bucket bandwidthLimiter) {
this.downloadCounter = downloadCounter;
this.bandwidthLimiter = bandwidthLimiter;
}
@GetMapping("/{fileId}")
public void downloadFile(@PathVariable String fileId, HttpServletResponse response) throws IOException {
long currentCount = downloadCounter.getCurrentCount();
if (currentCount >= MAX_CONCURRENT_DOWNLOADS) {
response.setStatus(HttpServletResponse.SC_TOO_MANY_REQUESTS);
response.getWriter().write("Too many downloads. Please try again later.");
return;
}
downloadCounter.increment();
try {
// 设置响应头
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition", "attachment; filename=" + fileId + ".bin");
ServletOutputStream out = response.getOutputStream();
// 文件路径(示例)
String filePath = "/path/to/files/" + fileId + ".bin";
if (!Files.exists(Paths.get(filePath))) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
response.getWriter().write("File not found.");
return;
}
byte[] buffer = new byte[8192]; // 每次读取 8KB
RandomAccessFile file = new RandomAccessFile(filePath, "r");
int bytesRead;
while ((bytesRead = file.read(buffer)) != -1) {
if (bytesRead > 0) {
boolean consumed = bandwidthLimiter.tryConsume(bytesRead);
if (!consumed) {
Thread.sleep(100); // 等待令牌生成
continue;
}
out.write(buffer, 0, bytesRead);
out.flush();
}
}
file.close();
out.close();
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 恢复中断状态
} finally {
downloadCounter.decrement();
}
}
}
🔁 5. 动态调整下载速率接口(可选)
java
@RestController
@RequestMapping("/api/limit")
public class RateLimitController {
private final Bucket bandwidthLimiter;
private final Bandwidth globalBandwidthLimit;
public RateLimitController(Bucket bandwidthLimiter, Bandwidth globalBandwidthLimit) {
this.bandwidthLimiter = bandwidthLimiter;
this.globalBandwidthLimit = globalBandwidthLimit;
}
@PostMapping("/set-bandwidth")
public String setBandwidth(@RequestParam int mbPerSecond) {
Bandwidth newLimit = Bandwidth.classic(mbPerSecond * 1024 * 1024,
Refill.greedy(mbPerSecond * 1024 * 1024, Duration.ofSeconds(1)));
bandwidthLimiter.replaceConfiguration(newLimit);
return "Global bandwidth limit updated to " + mbPerSecond + " MB/s";
}
}
📊 6. 监控接口(可选)
java
@GetMapping("/monitor/concurrent")
public ResponseEntity<Long> getConcurrentDownloads() {
return ResponseEntity.ok(downloadCounter.getCurrentCount());
}
🧪 7. 测试建议
你可以使用 curl
或 Postman 发起多并发请求测试:
bash
for i in {1..200}; do
curl -X GET "http://localhost:8080/api/download/file1" --output "file$i.bin" &
done
观察是否触发限流、并发控制是否生效。
✅ 总结
组件 | 作用 |
---|---|
Redis | 分布式存储并发数和限流令牌桶 |
Lua 脚本 | 原子操作并发计数器 |
Bucket4j + Redis | 全局下载速率限制 |
Spring Boot Controller | 处理下载逻辑 |
try-finally | 保证资源释放 |
动态接口 /set-bandwidth |
支持运行时修改限速 |
📌 扩展建议(可选)
- 将限流逻辑封装到 AOP 切面 中
- 添加 Prometheus 指标暴露并发数、限流次数等
- 使用 Nginx 或 Gateway 做额外的限流保护
- 加入用户身份识别,支持 用户级限速
- 使用 Kafka 异步记录日志或审计下载行为