后端下载限速(redis记录实时并发,bucket4j动态限速)

  • ✅ 使用 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 异步记录日志或审计下载行为

相关推荐
Zfox_3 小时前
Redis:Hash数据类型
服务器·数据库·redis·缓存·微服务·哈希算法
呼拉拉呼拉3 小时前
Redis内存淘汰策略
redis·缓存
陈丹阳(滁州学院)5 小时前
若依添加添加监听容器配置(删除键,键过期)
数据库·oracle
远方16095 小时前
14-Oracle 23ai Vector Search 向量索引和混合索引-实操
数据库·ai·oracle
GUIQU.7 小时前
【Oracle】数据仓库
数据库·oracle
恰薯条的屑海鸥7 小时前
零基础在实践中学习网络安全-皮卡丘靶场(第十六期-SSRF模块)
数据库·学习·安全·web安全·渗透测试·网络安全学习
咖啡啡不加糖7 小时前
Redis大key产生、排查与优化实践
java·数据库·redis·后端·缓存
曼汐 .7 小时前
数据库管理与高可用-MySQL高可用
数据库·mysql
MickeyCV7 小时前
使用Docker部署MySQL&Redis容器与常见命令
redis·mysql·docker·容器·wsl·镜像
2301_793102497 小时前
Linux——MySql数据库
linux·数据库