百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

一文搞懂!7种常见的限流方式!(限流手段)

mhr18 2025-04-09 18:00 21 浏览 0 评论

在实际应用中,每个系统或者服务都有其处理能力的极限(瓶颈),即便是微服务中有集群和分布式的夹持,也不能保证系统能应对任何大小的流量,因此,系统为了自保,需要对处理能力范围以外的流量进行“特殊照顾”(比如,丢弃请求或者延迟处理),从而避免系统卡死、崩溃或不可用等情况,保证系统整体服务可用。

这篇文章,我们来分析7种常见的限流方式。

1. 令牌桶算法

令牌桶算法(Token Bucket Algorithm)是计算机网络和电信领域中常用的一种简单方法,用于流量整形和速率限制。它旨在控制系统在某个时间段内可以发送或接收的数据量,确保流量符合指定的速率。

令牌桶算法的核心思路:系统按照固定速度往桶里加入令牌,如果桶满则停止添加。当有请求到来时,会尝试从桶里拿走一个令牌,取到令牌才能继续进行请求处理,没有令牌就拒绝服务。示意图如下:


令牌桶法的几个特点:

  1. 令牌桶容量固定,即系统的处理能力阈值
  2. 令牌放入桶内的速度固定
  3. 令牌从桶内拿出的速度根据实际请求量而定,每个请求对应一个令牌
  4. 当桶内没有令牌时,请求进入等待或者被拒绝

令牌桶算法主要用于应对突发流量的场景,在 Java语言中使用最多的是 Google的 Guava RateLimiter,下面举几个例子来说明它是如何应对突发流量:

示例1

import java.util.concurrent.TimeUnit;
public class RateLimit {

  public static void main(String[] args) {
    RateLimiter limiter = RateLimiter.create(5); // 每秒创建5个令牌
    System.out.println("acquire(5), wait " + limiter.acquire(5) + " s"); // 全部取走 5个令牌
    System.out.println("acquire(1), wait " + limiter.acquire(1) + " s");// 获取1个令牌
    boolean result = limiter.tryAcquire(1, 0, TimeUnit.SECONDS); // 尝试获取1个令牌,获取不到则直接返回
    System.out.println("tryAcquire(1), result: " + result);
  }
}

示例代码运行结果如下:

acquire(5), wait 0.0 s
acquire(1), wait 0.971544 s
tryAcquire(1), result: false

桶中共有 5个令牌,acquire(5)返回0 代表令牌充足无需等待,当桶中令牌不足,acquire(1)等待一段时间才获取到,当令牌不足时,tryAcquire(1)不等待直接返回。

示例2

import com.google.common.util.concurrent.RateLimiter;
public class RateLimit {
    public static void main(String[] args) {
        RateLimiter limiter = RateLimiter.create(5);
        System.out.println("acquire(10), wait " + limiter.acquire(10) + " s");
        System.out.println("acquire(1), wait " + limiter.acquire(1) + " s");
    }
}

示例代码运行结果如下:

acquire(10), wait 0.0 s
acquire(1), wait 1.974268 s

桶中共有 5个令牌,acquire(10)返回0,和示例似乎有点冲突,其实,这里返回0 代表应对了突发流量,但是 acquire(1) 却等待了 1.974268秒,这代表 acquire(1)的等待是时间包含了应对突然流量多出来的 5个请求,即 1.974268 = 1 + 0.974268。

为了更好的验证示例2的猜想,我们看示例3:

示例3

import com.google.common.util.concurrent.RateLimiter;
import java.util.concurrent.TimeUnit;
public class RateLimit {
    public static void main(String[] args) throws InterruptedException {
        RateLimiter limiter = RateLimiter.create(5);
        System.out.println("acquire(5), wait " + limiter.acquire(5) + " s");
        TimeUnit.SECONDS.sleep(1);
        System.out.println("acquire(5), wait " + limiter.acquire(5) + " s");
        System.out.println("acquire(1), wait " + limiter.acquire(1) + " s");
    }
}

示例代码运行结果如下:

acquire(5), wait 0.0 s
acquire(5), wait 0.0 s
acquire(1), wait 0.966104 s

桶中共有 5个令牌,acquire(5)返回0 代表令牌充足无需等待,接着睡眠 1s,这样系统又可以增加5个令牌, 因此,再次 acquire(5)令牌充足返回0 无需等待,acquire(1)需要等待一段时间才能获取令牌。

2 漏桶算法

漏桶算法(Leaky Bucket Algorithm)的核心思路是:水(请求)进入固定容量的漏桶,漏桶的水以固定的速度流出,当水流入漏桶的速度过大导致漏桶满而直接溢出,然后拒绝请求。示意图如下:


下面为一个 Java版本的漏桶算法示例:

import java.util.concurrent.*;
publicclass LeakyBucket {
    privatefinalint capacity; // 桶的容量
    privatefinalint rate;     // 出水速率
    privateint water;          // 漏斗中的水量
    privatelong lastLeakTime;  // 上一次漏水的时间

    public LeakyBucket(int capacity, int rate) {
        this.capacity = capacity;
        this.rate = rate;
        this.water = 0;
        this.lastLeakTime = System.currentTimeMillis();
    }

    public synchronized boolean allowRequest(int tokens) {
        leak(); // 漏水
        if (water + tokens <= capacity) {
            water += tokens; // 漏斗容量未满,可以加水
            returntrue;
        } else {
            returnfalse; // 漏斗容量已满,无法加水
        }
    }

    private void leak() {
        long currentTime = System.currentTimeMillis();
        long timeElapsed = currentTime - lastLeakTime;
        int waterToLeak = (int) (timeElapsed * rate / 1000); // 计算经过的时间内应该漏掉的水量
        water = Math.max(0, water - waterToLeak); // 漏水
        lastLeakTime = currentTime; // 更新上一次漏水时间
    }

    public static void main(String[] args) {
        LeakyBucket bucket = new LeakyBucket(10, 2); // 容量为10,速率为2令牌/秒
        int[] packets = {2, 3, 1, 5, 2, 10}; // 要发送的数据包大小

        for (int packet : packets) {
            if (bucket.allowRequest(packet)) {
                System.out.println("发送 " + packet + " 字节的数据包");
            } else {
                System.out.println("漏桶已满,无法发送数据包");
            }
            try {
                TimeUnit.SECONDS.sleep(1); // 模拟发送间隔
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

漏桶算法的几个特点:

  1. 漏桶容量固定
  2. 流入(请求)速度随意
  3. 流出(处理请求)速度固定
  4. 桶满则溢出,即拒绝新请求(限流)

3. 计数器算法

计数器是最简单的限流方式,主要用来限制总并发数,主要通过一个支持原子操作的计数器来累计 1秒内的请求次数,当 秒内计数达到限流阈值时触发拒绝策略。每过 1秒,计数器重置为 0开始重新计数。比如数据库连接池大小、线程池大小、程序访问并发数等都是使用计数器算法。

如下代码就是一个Java版本的计数器算法示例,通过一个原子计算器 AtomicInteger来记录总数,如果请求数大于总数就拒绝请求,否则正常处理请求:

import java.util.concurrent.atomic.AtomicInteger;
publicclass CounterRateLimiter {
    privatefinalint limit;          // 限流阈值
    privatefinallong windowSizeMs;  // 时间窗口大小(毫秒)
    private AtomicInteger counter;    // 请求计数器
    privatelong lastResetTime;       // 上次重置计数器的时间

    public CounterRateLimiter(int limit, long windowSizeMs) {
        this.limit = limit;
        this.windowSizeMs = windowSizeMs;
        this.counter = new AtomicInteger(0);
        this.lastResetTime = System.currentTimeMillis();
    }

    public boolean allowRequest() {
        long currentTime = System.currentTimeMillis();
        // 如果当前时间超出了时间窗口,重置计数器
        if (currentTime - lastResetTime > windowSizeMs) {
            counter.set(0);
            lastResetTime = currentTime;
        }
        // 检查计数器是否超过了限流阈值
        return counter.incrementAndGet() <= limit;
    }

    public static void main(String[] args) {
        CounterRateLimiter rateLimiter = new CounterRateLimiter(3, 1000); // 每秒最多处理3个请求
        for (int i = 0; i < 10; i++) {
            if (rateLimiter.allowRequest()) {
                System.out.println("允许请求 " + (i + 1));
            } else {
                System.out.println("限流,拒绝请求 " + (i + 1));
            }
            try {
                Thread.sleep(200); // 模拟请求间隔
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

4. 固定窗口限流算法

固定窗口(Fixed Window)限流算法基于时间窗口(如1秒、1分钟)来统计请求数。当请求数量超过预设的阈值时,超过部分的请求将被拒绝或延迟处理。

实现方式:

  • 将时间划分为固定长度的窗口。
  • 维护一个计数器,在当前窗口内统计请求数。
  • 每个请求到来时,检查当前窗口的计数器是否超过阈值。未超过则允许请求并增加计数器,超过则拒绝请求。

优点

  • 实现简单,容易理解和部署。
  • 对于流量变化不频繁的场景效果较好。缺点
  • 存在桶边缘问题,即在窗口切换瞬间可能会短时间内允许大量请求,导致瞬时流量激增。
  • 精度较低,无法平滑地限制请求速率。适用场景
  • 对于流量均匀且突发性要求不高的应用场景。
  • 单机或小规模分布式系统中。

5. 滑动窗口算法

滑动窗口算法是一种常用于限流和统计的算法。它基于一个固定大小的时间窗口,在这个时间窗口内统计请求的数量, 并根据设定的阈值来控制流量。比如,TCP协议就使用了该算法

以下是一个简单的 Java 示例实现滑动窗口算法:

import java.util.concurrent.atomic.AtomicInteger;

publicclass SlidingWindowRateLimiter {
    privatefinalint limit;          // 限流阈值
    privatefinallong windowSizeMs;  // 时间窗口大小(毫秒)
    privatefinal AtomicInteger[] window;  // 滑动窗口
    privatelong lastUpdateTime;      // 上次更新窗口的时间
    privateint pointer;              // 指向当前时间窗口的指针

    public SlidingWindowRateLimiter(int limit, long windowSizeMs, int granularity) {
        this.limit = limit;
        this.windowSizeMs = windowSizeMs;
        this.window = new AtomicInteger[granularity];
        for (int i = 0; i < granularity; i++) {
            window[i] = new AtomicInteger(0);
        }
        this.lastUpdateTime = System.currentTimeMillis();
        this.pointer = 0;
    }

    public synchronized boolean allowRequest() {
        long currentTime = System.currentTimeMillis();
        // 计算时间窗口的起始位置
        long windowStart = currentTime - windowSizeMs + 1;

        // 更新窗口中过期的计数器
        while (lastUpdateTime < windowstart lastupdatetime windowpointer.set0 pointer='(pointer' 1 window.length int totalrequests='0;' for atomicinteger counter : window totalrequests if totalrequests>= limit) {
            returnfalse; // 超过限流阈值,拒绝请求
        } else {
            window[pointer].incrementAndGet(); // 记录新的请求
            returntrue; // 允许请求
        }
    }

    public static void main(String[] args) {
        SlidingWindowRateLimiter rateLimiter = new SlidingWindowRateLimiter(10, 1000, 10); // 每秒最多处理10个请求
        for (int i = 0; i < 20; i++) {
            if (rateLimiter.allowRequest()) {
                System.out.println("允许请求 " + (i + 1));
            } else {
                System.out.println("限流,拒绝请求 " + (i + 1));
            }
            try {
                Thread.sleep(100); // 模拟请求间隔
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

6. Redis + Lua分布式限流

Redis + Lua属于分布式环境下的限流方案,主要利用的是Lua在 Redis中运行能保证原子性。如下示例为一个简单的Lua限流脚本:

local key = KEYS[1]
local limit = tonumber(ARGV[1])
local current = tonumber(redis.call('get', key) or "0")

if current + 1 > limit then
    return 0
else
    redis.call("INCRBY", key, 1)
    redis.call("EXPIRE", key, 1)
    return 1
end

脚本解释:

  • KEYS[1]:限流的键名,注意,在Lua中,下角标是从 1开始
  • ARGV[1]:限流的最大值
  • redis.call('get', key):获取当前限流计数。
  • redis.call('INCRBY', key, 1):增加限流计数。
  • redis.call('EXPIRE', key, 1):设置键的过期时间为 1 秒。

7. 三方工具

当我们自己无法实现比较好的限流方案时,成熟的三方框架就是我们比较好的选择,下面列出两个 Java语言比较优秀的框架。

7.1 resilience4j

resilience4j 是一个轻量级的容错库,提供了限流、熔断、重试等功能。限流模块 RateLimiter 提供了灵活的限流配置,其优点如下:

  • 集成了多种容错机制
  • 支持注解方式配置
  • 易于与 Spring Boot集成

7.2 Sentinel

Sentinel 是阿里巴巴开源的一个功能全面的流量防护框架,提供限流、熔断、系统负载保护等多种功能。其优点如下:

  • 功能全面,适用于多种场景
  • 强大的监控和控制台
  • 与 Spring Cloud 深度集成

8. 总结

本文我们分析了7种常见的限流方式:

  1. 令牌桶
  2. 漏桶
  3. 计数器
  4. 固定窗口
  5. 滑动窗口
  6. Redis + Lua 分布式限流
  7. 三方工具

上面的限流方式,主要是针对服务器进行限流,除此之外,我们也可以对客户端进行限流, 比如验证码,答题,排队等方式。

另外,我们也会在一些中间件上进行限流,比如Apache、Tomcat、Nginx等。

在实际的开发中,限流场景略有差异,限流的维度也不一样,比如,有的场景需要根据请求的 URL来限流,有的会对 IP地址进行限流、另外,设备ID、用户ID 也是限流常用的维度,因此,我们需要结合真实业务场景灵活的使用限流方案。

相关推荐

【推荐】一个开源免费、AI 驱动的智能数据管理系统,支持多数据库

如果您对源码&技术感兴趣,请点赞+收藏+转发+关注,大家的支持是我分享最大的动力!!!.前言在当今数据驱动的时代,高效、智能地管理数据已成为企业和个人不可或缺的能力。为了满足这一需求,我们推出了这款开...

Pure Storage推出统一数据管理云平台及新闪存阵列

PureStorage公司今日推出企业数据云(EnterpriseDataCloud),称其为组织在混合环境中存储、管理和使用数据方式的全面架构升级。该公司表示,EDC使组织能够在本地、云端和混...

对Java学习的10条建议(对java课程的建议)

不少Java的初学者一开始都是信心满满准备迎接挑战,但是经过一段时间的学习之后,多少都会碰到各种挫败,以下北风网就总结一些对于初学者非常有用的建议,希望能够给他们解决现实中的问题。Java编程的准备:...

SQLShift 重大更新:Oracle→PostgreSQL 存储过程转换功能上线!

官网:https://sqlshift.cn/6月,SQLShift迎来重大版本更新!作为国内首个支持Oracle->OceanBase存储过程智能转换的工具,SQLShift在过去一...

JDK21有没有什么稳定、简单又强势的特性?

佳未阿里云开发者2025年03月05日08:30浙江阿里妹导读这篇文章主要介绍了Java虚拟线程的发展及其在AJDK中的实现和优化。阅前声明:本文介绍的内容基于AJDK21.0.5[1]以及以上...

「松勤软件测试」网站总出现404 bug?总结8个原因,不信解决不了

在进行网站测试的时候,有没有碰到过网站崩溃,打不开,出现404错误等各种现象,如果你碰到了,那么恭喜你,你的网站出问题了,是什么原因导致网站出问题呢,根据松勤软件测试的总结如下:01数据库中的表空间不...

Java面试题及答案最全总结(2025版)

大家好,我是Java面试陪考员最近很多小伙伴在忙着找工作,给大家整理了一份非常全面的Java面试题及答案。涉及的内容非常全面,包含:Spring、MySQL、JVM、Redis、Linux、Sprin...

数据库日常运维工作内容(数据库日常运维 工作内容)

#数据库日常运维工作包括哪些内容?#数据库日常运维工作是一个涵盖多个层面的综合性任务,以下是详细的分类和内容说明:一、数据库运维核心工作监控与告警性能监控:实时监控CPU、内存、I/O、连接数、锁等待...

分布式之系统底层原理(上)(底层分布式技术)

作者:allanpan,腾讯IEG高级后台工程师导言分布式事务是分布式系统必不可少的组成部分,基本上只要实现一个分布式系统就逃不开对分布式事务的支持。本文从分布式事务这个概念切入,尝试对分布式事务...

oracle 死锁了怎么办?kill 进程 直接上干货

1、查看死锁是否存在selectusername,lockwait,status,machine,programfromv$sessionwheresidin(selectsession...

SpringBoot 各种分页查询方式详解(全网最全)

一、分页查询基础概念与原理1.1什么是分页查询分页查询是指将大量数据分割成多个小块(页)进行展示的技术,它是现代Web应用中必不可少的功能。想象一下你去图书馆找书,如果所有书都堆在一张桌子上,你很难...

《战场兄弟》全事件攻略 一般事件合同事件红装及隐藏职业攻略

《战场兄弟》全事件攻略,一般事件合同事件红装及隐藏职业攻略。《战场兄弟》事件奖励,事件条件。《战场兄弟》是OverhypeStudios制作发行的一款由xcom和桌游为灵感来源,以中世纪、低魔奇幻为...

LoadRunner(loadrunner录制不到脚本)

一、核心组件与工作流程LoadRunner性能测试工具-并发测试-正版软件下载-使用教程-价格-官方代理商的架构围绕三大核心组件构建,形成完整测试闭环:VirtualUserGenerator(...

Redis数据类型介绍(redis 数据类型)

介绍Redis支持五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)及Zset(sortedset:有序集合)。1、字符串类型概述1.1、数据类型Redis支持...

RMAN备份监控及优化总结(rman备份原理)

今天主要介绍一下如何对RMAN备份监控及优化,这里就不讲rman备份的一些原理了,仅供参考。一、监控RMAN备份1、确定备份源与备份设备的最大速度从磁盘读的速度和磁带写的带度、备份的速度不可能超出这两...

取消回复欢迎 发表评论: