限流--4种经典限流算法讲解--单机限流和分布式限流的实现

原文链接:https://blog.csdn.net/m0_59084856/article/details/138284371

为什么需要限流
系统的维护使用是需要成本的,用户可能使用科技疯狂刷量,消耗系统资源,出现额外的经济开销
问题:

控制成本=>限制用户的调用次数
用户在短时间内疯狂使用,导致服务器资源被占满,其他用户无法使用=>限流
那么限流阈值多大合适?比如限制单个用户在每秒只能使用1次。

限流的算法
推荐阅读:https://juejin.cn/post/6967742960540581918

1)固定窗口限流

单位时间内允许部分操作
1小时只允许10个用户操作

优点:最简单
缺点:可能出现流量突刺
比如:前59分钟没有1个操作,第59分钟来了10个操作;第1小时又来了10个操作。相当于2分钟内执行了20个操作,服务器仍然有高峰危险。

2)滑动窗口限流

单位时间内允许部分操作,但是这个单位时间是滑动的,需要指定一个滑动单位。
滑动窗口与固定窗口相比,将一个时间段又划分成了几个更小的切片。随着时间的前进,滑动窗口不断向前加载切片,向后遗弃切片,但是切片的总长度是固定的,滑动窗口保证了自己时间段内所有的访问不会超过阈值。

优点:能够解决流量突刺问题,第59分钟和第1小时分为了两个切片,但是属于一个时间段,更多的操作会被拒绝
缺点:实现相对复杂,限流效果和滑动单位有关,滑动单位越小,限流效果越好,但往往很难选取到一个特别合适的滑动单位。

3)漏桶限流(推荐)

以固定的速率处理请求(漏水),当请求桶满了后,拒绝请求。
每秒处理10个请求,同的容量是10,每0.1秒固定处理一次请求,如果1秒来了10个请求;都可以处理完,但如果1秒内来了11个请求,最后那个请求就会溢出桶,被拒绝。

优点:能够一定程度上应对流量突刺,能够以固定速率处理请求,保证服务器的安全
缺点:没有办法迅速处理一批请求,只能一个一个按顺序来处理(固定速率的缺点)

4)令牌桶限流(推荐)

管理员先生成一批令牌,每秒生成10个令牌;当用户要操作前,先去拿到一个令牌,有令牌的人就有资格执行操作、能同时执行操作;拿不到令牌就等着

优点:能够并发处理同时的请求,并发性能会更高
需要考虑的问题:还是存在时间单位选取的问题

想看这些算法的具体实现的话,可以参考这篇文章:4种经典限流算法讲解
在实际开发中,我们一般调用第三方库,无需关注这些算法的具体实现,只需要理解上边我说的这些算法的思想就行

限流粒度
针对某个方法限流,即单位时间内最多允许同时XX个操作使用这个方法
针对某个用户限流,比如单个用户单位时间内最多执行XX次操作
针对某个用户X方法限流,比如单个用户单位时间内最多执行XX次这个方法
限流的实现

1)本地限流(单机限流)

每个服务器单独限流,一般适用于单体项目,就是项目只有一个服务器

在Java中,使用Guava库实现单机限流:

Guava库提供了RateLimiter类,它使用令牌桶算法来控制请求的速率。

添加依赖:首先需要在项目的pom.xml文件中添加Guava库的依赖。

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>30.1-jre</version> <!-- 请使用最新版本 -->
</dependency>

创建RateLimiter实例:创建一个RateLimiter实例,设置每秒可以处理的请求数。

import com.google.common.util.concurrent.RateLimiter;

RateLimiter rateLimiter = RateLimiter.create(5.0); // 每秒5个请求

限流操作:在需要限流的方法调用前,调用rateLimiter.acquire()来获取一个令牌。

public void doSomething() {
    if (rateLimiter.tryAcquire()) {
        // 执行操作
    } else {
        // 限流逻辑,如等待或返回错误
    }
}

4.**如何在第3点中的else里写等待逻辑:**可以实现几种不同的等待逻辑:

主动等待:使用 Thread.sleep() 或其他同步等待方法等待一段时间后再次尝试获取令牌。
被动等待:实现一个循环,不断尝试获取令牌,直到成功为止。
随机等待:使用随机等待时间以避免多个请求同时触发。
指数退避:等待时间逐渐增加,直到成功获取令牌。
返回错误:如果等待时间过长,可以选择返回错误,避免无限期的等待。
以下是几种等待逻辑的示例代码:

主动等待

解释

public void doSomething() {
    if (rateLimiter.tryAcquire()) {
        // 执行操作
    } else {
        try {
            // 等待一段时间后再次尝试,例如等待500毫秒
            Thread.sleep(500);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            // 可以选择返回错误或继续执行
        }
        // 再次尝试获取令牌
        if (rateLimiter.tryAcquire()) {
            // 执行操作
        } else {
            // 如果再次获取失败,可以选择返回错误或继续等待
        }
    }
}

被动等待(不断尝试)

解释

public void doSomething() {
    while (!rateLimiter.tryAcquire()) {
        // 可以选择在这里实现一个短暂的等待,如Thread.sleep(10),以避免CPU占用过高
    }
    // 执行操作
}

随机等待

解释

import java.util.Random;

public void doSomething() {
    Random random = new Random();
    while (!rateLimiter.tryAcquire()) {
        try {
            // 随机等待一段时间,例如1到500毫秒之间
            int waitTime = random.nextInt(500);
            Thread.sleep(waitTime);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            // 可以选择返回错误或继续执行
        }
    }
    // 执行操作
}

指数退避

复制解释

public void doSomething() {
    int retries = 0;
    while (!rateLimiter.tryAcquire()) {
        try {
            // 指数退避等待时间
            Thread.sleep((int) (Math.pow(2, retries) * 100));
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            // 可以选择返回错误或继续执行
        }
        retries++;
        if (retries > MAX_RETRIES) {
            // 超过最大重试次数,可以选择返回错误
            break;
        }
    }
    // 执行操作
}

在实际应用中,选择哪种等待逻辑取决于你的具体需求和场景。例如,如果你希望避免重试对系统造成额外压力,可以选择随机等待或指数退避。如果你希望确保请求最终能够被处理,可以选择被动等待。如果你希望快速失败,可以选择返回错误。

2)分布式限流(多机限流)
如果项目有多个服务器,比如微服务,那么建议使用分布式限流。

把用户的使用频率等数据放到一个集中的存储进行统计,比如Redis,这样无论用户的请求落到了哪台服务器,都以集中的数据存储内的数据为准(Redisson - 是一个操作Redis的工具库)
在网关集中进行限流和统计(比如Sentinel、Spring Cloud Gateway)

import org.redisson.Redisson;
import org.redisson.api.RSemaphore;
import org.redisson.api.RedissonClient;

public static void main(String[] args){
    //创建RedissonClient
    RedissonClient redisson = Redisson.create();

    //获取限流器
    RRateLimiter semaphore = redisson.getRateLimiter("mySemaphore");

    //尝试获取许可证
    boolean result = semaphore.tryAcquire();
    if(result){
        //处理请求
    }else{
        //超过流量限制,需要做何处理
    }

}

Redisson限流实现
Redisson内置了一个限流工具类,可以借助Redis来存储统计。
官方仓库:https://github.com/redisson/redisson
看不懂方法的参数含义怎么办?

看官方文档
下载源码

点进任意一个包里的代码,然后点击下载源代码即可自动下载

步骤:

安装Redis
引入Redisson代码包:

        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.21.3</version>
        </dependency>

创建RedissonConfig配置块,用于初始化RedissonClient对象单例

spring:
  redis:
    host: 127.0.0.1
    port: 6379
    database: 0
@Configuration
@ConfigurationProperties(prefix = "spring.redis")
@Data
public class RedissonConfig {
    private Integer database;
    private String host;
    private Integer port;
    private String password;

    @Bean
    public RedissonClient getRedissonClient() {
        Config config = new Config();
        config.useSingleServer()
                .setDatabase(database)
                .setAddress("redis://" + host + ":" + port)
                .setPassword(password);
        RedissonClient redissonClient = Redisson.create(config);
        return redissonClient;
    }
}

编写RedisLimiterManager
什么是Manager?提供了通用的能力,可以放到任何一个项目里

/**
 * 专门提供RedisLimiter限流基础服务(提供了通用的能力)
 */
@Service
public class RedisLimiterManager {

    @Resource
    private RedissonClient redissonClient;

    /**
     * 限流操作
     * @param key 区分不同的限流器,比如不同的用户id应该分别统计
     */
    public void doRateLimit(String key) {
        //创建一个名称为key的限流器,每秒最多访问2次
        RRateLimiter rateLimiter = redissonClient.getRateLimiter(key);
        rateLimiter.trySetRate(RateType.OVERALL, 2, 1, RateIntervalUnit.SECONDS);

        //每当一个操作来了之后,请求一个令牌
        boolean canOp = rateLimiter.tryAcquire(1);
        if (!canOp) {
            throw new BusinessException(ErrorCode.TOO_MANY_REQUEST);
        }
    }
}

单元测试:

@SpringBootTest
class RedisLimiterManagerTest {

    @Resource
    private RedisLimiterManager redisLimiterManager;

    @Test
    void doRateLimit() throws InterruptedException {
        String userId = "1";
        for (int i = 0; i < 2; i++) {
            redisLimiterManager.doRateLimit(userId);
            System.out.println("成功");
        }
        Thread.sleep(2000);
        for (int i = 0; i < 5; i++) {
            redisLimiterManager.doRateLimit(userId);
            System.out.println("成功");
        }
    }
}

应用到要限流的方法中,比如智能分析接口:
//必须登录

User loginUser = userService.getLoginUser(request);
//限流判断,每个用户一个限流器
redisLimiterManager.doRateLimit("genChartByAi_" + loginUser.getId());
<think>我们正在讨论Sentinel的限流实现原理。根据引用内容,Sentinel的限流主要基于预先设定的规则,包括QPS限流并发线程数限流等。同时,引用[2]提到了令牌桶算法的一个简化实现,引用[3]则介绍了冷启动的预热机制。因此,我们需要从以下几个方面来详细解释Sentinel的限流原理: 1. **限流规则**:Sentinel支持多种维度的限流规则,包括QPS、并发线程数、系统负载等。 2. **限流算法**:Sentinel底层使用了多种算法,包括滑动窗口、令牌桶、漏桶等,以及预热排队等待等高级特性。 3. **实现机制**:Sentinel通过责任链模式,在调用链路上插入各种Slot(包括统计、限流等)来实现限流。 接下来,我们将详细展开这些内容。 ### 一、Sentinel限流核心原理 Sentinel的限流功能基于以下核心组件: #### 1. **流量控制规则(FlowRule)** - **资源名(resource)**:被保护的资源,如接口、方法等。 - **限流阈值(count)**:单位时间内允许通过的请求数(QPS)或并发线程数。 - **流控模式**: - 直接模式:直接对当前资源限流- 关联模式:当关联资源达到阈值时,限流当前资源(适用于应用让步场景)。 - 链路模式:只统计指定链路上的流量。 - **流控效果**: - 快速失败:直接抛出`FlowException`。 - Warm Up:冷启动预热(引用[3]),防止冷系统被压垮。 - 排队等待:匀速通过请求(基于漏桶算法)。 #### 2. **限流算法实现** Sentinel内部使用**滑动窗口统计**实时QPS,结合多种算法进行限流判断: | 算法类型 | 作用原理 | 适用场景 | |----------------|--------------------------------------------------------------------------|------------------------------| | **滑动窗口** | 将时间划分为多个小窗口,统计最近N个窗口的请求总数 | 实时QPS统计(核心基础) | | **令牌桶** | 以恒定速率生成令牌,请求获取令牌才能通过(引用[2]的简化示例) | 允许突发流量(如网络设备) | | **漏桶** | 以恒定速率处理请求,超出桶容量的请求被丢弃 | 流量整形(如排队等待效果) | | **并发计数器** | 直接统计当前处理中的线程数,超过阈值则拒绝 | 保护系统线程资源 | > 注:Warm Up效果通过**令牌桶变体**实现,初始令牌生成速率较慢,逐渐增加到设定值(引用[3])。 #### 3. **执行流程** Sentinel通过责任链调用执行限流检查: ```mermaid graph LR A[请求进入] --> B[NodeSelectorSlot] B --> C[ClusterBuilderSlot] C --> D[StatisticSlot] -- 统计QPS/线程数 --> E[FlowSlot] E -- 检查限流规则 --> F[返回结果] F --> G[执行被保护资源] ``` 关键Slot说明: - **StatisticSlot**:实时统计QPS、线程数、响应时间等指标。 - **FlowSlot**:调用`FlowRuleChecker`验证是否触发限流规则。 ### 二、Warm Up冷启动算法详解 引用[3]提到,Warm Up通过**冷启动因子(coldFactor)** 控制初始阈值。算法实现步骤如下: 1. **初始阈值** = 设定阈值 / coldFactor(默认3) 2. **预热时长**内线性增长至设定阈值 3. 期间若流量突增,仍按当前阈值限流 例如:设置QPS=300,预热时间10秒: - 第0秒:阈值=100 - 第5秒:阈值=200 - 第10秒:阈值=300 > 数学表达:设当前时间$t$($0 \leq t \leq T$,$T$为预热时间),则当前阈值$threshold(t) = \frac{t}{T} \times (maxThreshold - minThreshold) + minThreshold$,其中$minThreshold = maxThreshold / coldFactor$。 ### 三、排队等待的漏桶算法 当流控效果选择“排队等待”时,Sentinel使用漏桶算法控制请求匀速通过: 1. 设置**超时时间**(最大排队时长) 2. 请求进入桶内排队,按**固定间隔**释放 3. 超时未处理的请求将被拒绝 实现伪代码: ```java if (当前请求数 < 桶容量) { 放入队列并设置超时计时器; } else { 抛出FlowException; } ``` ### 四、完整限流判断流程 1. 请求进入`FlowSlot` 2. 遍历资源的`FlowRule`集合 3. 对每条规则依次检查: - 统计当前指标(QPS或线程数) - 根据流控模式判断是否触发阈值 - 若触发,按流控效果处理(快速失败/Warm Up/排队) 4. 全部规则通过则放行请求 ### 五、代码示例:令牌桶实现 基于引用[2]的简化令牌桶,Sentinel实际实现增加了预热控制: ```java public class WarmUpController { // 当前令牌数 protected double storedTokens; // 上次填充时间 protected long lastFilledTime; public boolean canPass(Node node, int acquireCount) { // 1. 计算当前令牌数(考虑时间间隔生成速率) long now = System.currentTimeMillis(); double newTokens = (now - lastFilledTime) * generateRate; storedTokens = Math.min(capacity, storedTokens + newTokens); lastFilledTime = now; // 2. 根据剩余令牌判断 if (storedTokens < acquireCount) { return false; // 触发限流 } else { storedTokens -= acquireCount; return true; } } } ``` ### 六、应用场景对比 | 限流策略 | 典型场景 | |----------------|--------------------------------------------------------------------------| | **QPS限流** | 保护API不被突发流量击穿(如秒杀活动) | | **线程数限流** | 防止慢调用耗尽线程池(如数据库查询阻塞) | | **Warm Up** | 服务刚启动时避免冷系统负载激增 | | **排队等待** | 消息队列消费场景,平滑处理请求峰值 | > Sentinel通过组合这些策略,实现对复杂分布式系统流量的精准控制[^1][^3]。 ### 七、常见问题解答 1. **Q:Sentinel与Guava RateLimiter有何区别?** - Sentinel支持集群限流熔断降级,而Guava仅本地限流 - Sentinel提供实时监控控制台配置,Guava需代码硬编码 2. **Q:如何选择QPS限流线程数限流?** - 保护系统吞吐量 → QPS限流 - 防止资源耗尽(如线程池) → 线程数限流 3. **Q:Warm Up冷启动因子如何设置?** - 默认3适用于多数场景,高并发系统可调至4~5[^3] §§相关问题§§ 1. Sentinel的熔断降级机制是如何工作的? 2. 如何实现Sentinel规则的动态配置? 3. Sentinel集群限流的核心技术是什么? [^1]: 【分布式】深入剖析 Sentinel 限流:原理、实现 [^2]: 一文讲解在Sentinel中涉及到的限流算法 [^3]: Sentinel限流规则详解</think>### Sentinel 限流算法实现原理详解 Sentinel 的限流核心基于**规则引擎 + 流量统计 + 算法控制**的三层架构,以下是详细解析: --- #### 一、限流规则维度 | 维度 | 说明 | 应用场景 | |--------------|----------------------------------------------------------------------|------------------------------| | **QPS限流** | 限制每秒请求量(如单资源阈值1000 QPS) | 防止API被突发流量击穿 | | **并发线程** | 控制同时处理的请求线程数(如数据库连接池限流50线程) | 保护资源不被慢调用耗尽 | | **系统负载** | 基于CPU/LOAD的综合控制(如LOAD>3时触发限流) | 保护宿主服务器稳定性 | | **黑白名单** | 按来源IP或用户进行访问控制 | 拦截恶意请求 | --- #### 二、核心限流算法实现 ##### 1. **滑动窗口统计(实时QPS计算)** ```java // 时间窗口分割(默认1秒=2个窗口) long[] windows = new long[2]; // 请求进入时更新计数 windows[currentIndex].addAndGet(1); ``` - **原理**:将1秒拆分为多个子窗口(默认500ms一个),统计时合并最近N个窗口计数 - **优势**:避免固定窗口的临界突变问题,精度提升10倍[^1] ##### 2. **令牌桶算法(允许突发流量)** 基于引用[2]的优化实现: ```java public boolean tryAcquire() { long now = System.nanoTime(); // 1. 计算新增令牌数(动态速率) double newTokens = (now - lastTime) * rate / 1e9; tokens = Math.min(capacity, tokens + newTokens); lastTime = now; // 2. 令牌不足时触发限流 if (tokens < 1) return false; // 3. 领取令牌 tokens -= 1; return true; } ``` - **突发控制**:桶容量=10时,允许瞬间10个请求通过 - **平滑限流**:令牌生成速率=100/s 则控制平均QPS为100 ##### 3. **预热算法(冷启动保护)** ```java // 冷启动因子coldFactor=3(引用[3]) double threshold = maxQps * Math.min(1, (warmupPeriod - elapsed) / warmupPeriod); ``` - **冷启动曲线**: $threshold(t) = \frac{t}{T} \times (maxQps - \frac{maxQps}{coldFactor}) + \frac{maxQps}{coldFactor}$ ($T$=预热时间,$t$=当前时间) - **效果**:初始阈值=33% maxQPS → 线性提升 → 100% maxQPS --- #### 三、执行流程剖析 ```mermaid graph TD A[请求进入] --> B{是否触发规则} B -->|是| C[执行流控效果] B -->|否| D[放行请求] C --> E[快速失败] C --> F[Warm Up] C --> G[排队等待] ``` 1. **规则匹配**:根据资源名匹配FlowRule 2. **指标统计**:通过滑动窗口计算实时QPS 3. **算法决策**: - 若QPS > 阈值 → 触发令牌桶 - 令牌桶拒绝 → 返回`Blocked by Sentinel` 4. **流控效果**: - **快速失败**:直接抛出FlowException - **Warm Up**:按冷启动曲线动态调整阈值 - **排队等待**:结合漏桶算法控制通过间隔 --- #### 四、高级场景实现 ##### 1. **集群限流** - **架构**:Token Server集群 → 统一令牌分配 - **同步机制**:AP架构(Raft协议保证一致性) ##### 2. **热点参数限流** ```java // 对特定参数单独计数 if ("user123".equals(param)) { user123Counter.add(1); } ``` - 场景:针对高频用户ID进行特殊限流 ##### 3. **自适应限流** - 基于实时指标动态调整规则: - RT > 500ms → 自动降低阈值20% - 成功率 < 95% → 触发熔断降级 --- #### 五、算法对比 | 算法 | 突发处理 | 平滑性 | 实现复杂度 | 适用场景 | |--------------|----------|--------|------------|------------------| | **令牌桶** | ✓ | 中 | 中 | 允许突发的API | | **漏桶** | ✗ | ✓ | 低 | 恒定速率场景 | | **滑动窗口** | ✗ | ✓ | 高 | 精准QPS控制 | | **计数器** | ✗ | ✗ | 低 | 简单粗暴限流 | > Sentinel **组合使用滑动窗口+令牌桶**,实现精准与突发兼顾的流量控制[^1][^2]。 --- #### 六、性能优化技巧 1. **无锁设计**: 使用`LongAdder`替代`AtomicLong`统计计数,减少CAS竞争 2. **时间缓存**: 每5ms获取一次System.nanoTime(),避免高频调用 3. **规则预编译**: 将FlowRule转换为执行树,减少匹配开销 > 经优化后,Sentinel单机限流损耗<1ms,吞吐量影响<3%[^1]。 --- ### 总结关键点 1. **统计是基础**:滑动窗口实现毫秒级精准统计 2. **算法是核心**:令牌桶控制突发,预热算法保护冷启动 3. **规则是灵魂**:支持多维度、多策略的流量塑形 4. **扩展是优势**:通过Slot责任链支持自定义限流逻辑 Sentinel通过算法组合+动态规则,实现从"粗暴拦截"到"智能流量塑形"的进化[^1][^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值