限流的方法,Redis 计算器限流算法、滑动时间窗口限流算法、漏漏桶限流算法、令牌桶限流算法,Java 开发

限流的方法,Redis 计算器限流算法、滑动时间窗口限流算法、漏漏桶限流算法、令牌桶限流算法,Java 开发

一、参考资料

【Java手写4种分布式限流算法(Sentinel限流/Redis计算器限流/滑动时间窗口/漏桶限流/令牌桶限流)1天学会,让你面试少走99%的弯路!】 https://www.bilibili.com/video/BV1yPA5e7ERU/?share_source=copy_web\&vd_source=855891859b2dc554eace9de3f28b4528

二、推荐使用 Redis 简单计数器限流算法

01、限制 1 秒内允许 100 个请求

其它的限流算法实现起来比较有难度,而且使用简单计数器限流算法满足大部分系统的需求,除非你国内顶级的淘宝这样的系统,需要顶级的完善的限流算法。

三、AI 回答,上图简单计算器限流算法能用吗

这段代码从语法上是正确的,能跑通,但在实际生产环境中存在严重的逻辑缺陷

简单来说:你实现的是**"全局限流"(所有用户共享一个额度),而不是通常需要的"单接口/单用户限流"**。

以下是详细的分析和改进建议:

❌ 当前代码存在的 3 个主要问题

1. 限流范围错误(最严重的问题)
  • 你的逻辑 :Key 是 System.currentTimeMillis() / 1000(即当前秒数,例如 1712345678)。
  • 后果 :这是一个全局计数器
    • 假设你有 1000 个用户。
    • 前 100 个用户访问成功。
    • 第 101 个用户访问时,计数器变成 101,直接返回"限流"。
    • 结果:剩下的 900 个用户全部无法访问。
  • 通常需求 :我们通常希望每个用户 (或每个 IP)每秒能访问 100 次,而不是所有用户加起来每秒只能访问 100 次。
2. 时间窗口边界问题("突刺"现象)
  • 你的逻辑 :使用 当前秒数 作为 Key。
  • 后果 :Redis 的 Key 会在每秒切换一次。
    • 场景 :假设在 12:00:01.900(第 1 秒末)来了 100 个请求,计数器满了。
    • 紧接着 :在 12:00:02.100(第 2 秒初)又来了 100 个请求。
    • 结果 :在短短 200 毫秒的时间差内,实际上处理了 200 个请求(第 1 秒的 100 个 + 第 2 秒的 100 个)。这违背了"每秒限流 100"的初衷。
3. Key 的过期时间(内存泄漏风险)
  • 问题:你没有设置 Key 的过期时间(TTL)。
  • 后果 :虽然 Key 是秒级的,看起来好像会自动"失效"(因为下一秒 Key 变了),但 Redis 中会永久保留历史秒的 Key(如 1712345670, 1712345671...)。随着时间推移,Redis 会存储数亿个无用的 Key,导致内存爆满。

✅ 改进方案

如果你只是想简单修复代码,使其符合"单用户限流",可以这样改:

方案一:基于用户的固定窗口限流(修复版)

将 Key 绑定到用户 IDIP 上,并设置过期时间。

java 复制代码
@GetMapping("/{id}")
public Result<DishVO> getById(@PathVariable Long id, HttpServletRequest request) {
    // 1. 获取用户唯一标识(例如 IP 地址,或者从 Token 中解析出的 userId)
    String ip = request.getRemoteAddr(); 
    
    // 2. 构造 Redis Key:固定前缀 + 用户标识 + 当前秒数
    // 这样不同用户的计数是隔离的
    String key = "rate_limit:dish:" + ip + ":" + (System.currentTimeMillis() / 1000);

    // 3. 自增
    Long count = redisTemplate.opsForValue().increment(key);

    // 4. 如果是第一次访问(count == 1),设置过期时间为 1 秒
    // 防止内存泄漏,让 Redis 自动删除过期的 Key
    if (count != null && count == 1) {
        redisTemplate.expire(key, 1, TimeUnit.SECONDS);
    }

    // 5. 判断是否超限
    if (count != null && count > 100) {
        return Result.error("限流:访问过于频繁");
    }

    log.info("根据 id 查询菜品,{}", id);
    DishVO dishVO = dishService.getByIdWithFlavor(id);
    return Result.success(dishVO);
}

方案二:使用 Redisson(推荐,生产级)

手动实现限流容易出 Bug(如上面的边界问题)。在生产环境中,强烈建议使用 Redisson 框架提供的 RRateLimiter,它实现了标准的令牌桶算法漏桶算法,更加平滑和精准。

引入依赖 (Maven):

xml 复制代码
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.20.0</version> <!-- 选最新版本 -->
</dependency>

代码实现:

java 复制代码
@Autowired
private RedissonClient redissonClient;

@GetMapping("/{id}")
public Result<DishVO> getById(@PathVariable Long id, HttpServletRequest request) {
    String ip = request.getRemoteAddr();
    
    // 获取限流器对象,Key 绑定 IP
    RRateLimiter rateLimiter = redissonClient.getRateLimiter("rate_limit:dish:" + ip);
    
    // 设置限流规则:每秒允许 100 个请求(令牌桶容量 100, refill 速率 100/s)
    // 只需要设置一次即可,实际项目中通常放在初始化代码块中
    rateLimiter.trySetRate(RateType.OVERALL, 100, 1, RateIntervalUnit.SECONDS);

    // 尝试获取许可
    if (rateLimiter.tryAcquire()) {
        // 获取成功,执行业务
        log.info("根据 id 查询菜品,{}", id);
        DishVO dishVO = dishService.getByIdWithFlavor(id);
        return Result.success(dishVO);
    } else {
        // 获取失败,限流
        return Result.error("限流:访问过于频繁");
    }
}

📌 总结

你的原始代码:

  • 能不能跑? 能。
  • 对不对? 不对。它是全局限流,且没有设置过期时间。
  • 建议: 至少加上 用户/IP 标识 作为 Key 的一部分,并加上 expire 设置。如果项目允许,请使用 Redisson

四、只需要设置一次即可,实际项目中通常放在初始化代码块中,怎么操作

这其实就是利用 Spring 的特性,在应用启动时只运行一次配置逻辑。最推荐的做法是写一个配置类,配合 @PostConstruct 注解,这样既符合 Spring 的规范,又能确保在 Bean 创建好后立即执行。

以下是具体的操作步骤和代码示例:

🛠️ 核心思路

  1. 创建一个专门的配置类(例如 RedissonConfig)。
  2. 注入 RedissonClient
  3. 使用 @PostConstruct 注解标记一个初始化方法。
  4. 在该方法中调用 trySetRate

💻 代码实现

java 复制代码
import org.redisson.api.RateIntervalUnit;
import org.redisson.api.RRateLimiter;
import org.redisson.api.RateType;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;

@Configuration
public class RedissonRateLimiterConfig {

    @Autowired
    private RedissonClient redissonClient;

    /**
     * 这个方法会在 RedissonConfig 这个 Bean 创建完成后自动执行
     * 且只会执行一次
     */
    @PostConstruct
    public void initRateLimiter() {
        // 1. 定义限流器的 Key(注意:这里定义的是"限流规则模板"的 Key,
        // 实际业务中每个用户会有独立的 Key,但规则是共享的,所以配置一次即可)
        String limiterKey = "rate_limit:dish:"; 

        // 2. 获取限流器对象
        RRateLimiter rateLimiter = redissonClient.getRateLimiter(limiterKey);

        // 3. 尝试设置限流规则
        // 参数说明:
        // RateType.OVERALL: 限流总量(所有实例共享配额,如果是集群模式)
        // 100: 速率(每秒允许 100 个请求)
        // 1: 时间间隔
        // RateIntervalUnit.SECONDS: 时间单位
        boolean isSet = rateLimiter.trySetRate(RateType.OVERALL, 100, 1, RateIntervalUnit.SECONDS);

        if (isSet) {
            System.out.println("✅ 限流规则初始化成功:100次/秒");
        } else {
            System.out.println("ℹ️ 限流规则已存在,跳过初始化");
        }
    }
}

💡 为什么要这样做?

性能优化

trySetRate 虽然是一个轻量级操作,但它仍然涉及网络通信(向 Redis 发送命令)。

  • 如果不放在初始化中 :每次用户请求接口时都调用一次 trySetRate,会造成不必要的网络开销。
  • 放在初始化中 :应用启动时设置一次,后续请求直接使用 tryAcquire() 即可,速度极快。
幂等性

Redisson 的 trySetRate 是幂等的。

  • 第一次调用:在 Redis 中写入配置。
  • 后续调用:发现配置已存在,直接忽略。
    所以即使你的服务重启了多次,或者有多个实例(在 Kubernetes 或 Docker 中),这个逻辑也是安全的。

⚠️ 进阶:如果是微服务集群怎么办?

如果你的应用部署了多个实例(例如 3 台服务器),上面的代码会在每台服务器启动时各执行一次 trySetRate。这通常没有问题,因为 trySetRate 会检查是否已存在。

但如果你希望更严谨 ,或者限流规则是动态配置在数据库/Nacos 中的,可以使用 Spring 的事件监听

java 复制代码
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component
public class RateLimiterInitializer {

    @Autowired
    private RedissonClient redissonClient;

    // 监听应用完全启动就绪的事件
    @EventListener(ApplicationReadyEvent.class)
    public void initOnStartup() {
        // 这里写初始化逻辑
        // 可以在这里从数据库读取动态的限流数值(比如 200 次/秒),而不是写死 100
        RRateLimiter rateLimiter = redissonClient.getRateLimiter("rate_limit:dish:");
        rateLimiter.trySetRate(RateType.OVERALL, 200, 1, RateIntervalUnit.SECONDS);
    }
}

📌 总结

对于你的场景,直接使用 @PostConstruct 是最简单、最标准的做法。把它放在一个单独的 Config 类里,保持代码整洁。

五、笔记总结

04、简单计数器限流算法实现





05、Redis 简单计数器限流算法实现

06、滑动时间窗口限流算法实现




07、滑动时间窗口算法高并发冲突问题


08、漏桶算法限流方法实现


09、令牌通限流算法实现


相关推荐
冬夜戏雪18 小时前
实习面经记录(十)
java·前端·javascript
skiy18 小时前
java与mysql连接 使用mysql-connector-java连接msql
java·开发语言·mysql
平生不喜凡桃李18 小时前
浅谈 Linux 中 namespace 相关系统调用
java·linux·服务器
zb2006412018 小时前
CVE-2024-38819:Spring 框架路径遍历 PoC 漏洞复现
java·后端·spring
2401_8955213418 小时前
spring-ai 下载不了依赖spring-ai-openai-spring-boot-starter
java·人工智能·spring
何仙鸟19 小时前
GarmageSet下载和处理
java·开发语言
wefly201719 小时前
免安装!m3u8live.cn在线 M3U8 播放器,小白也能快速上手
java·开发语言·python·json·php·m3u8·m3u8在线转换
yuweiade19 小时前
springboot和springframework版本依赖关系
java·spring boot·后端
ywf121519 小时前
springboot设置多环境配置文件
java·spring boot·后端
2401_8735449219 小时前
使用Python进行PDF文件的处理与操作
jvm·数据库·python