四种限流算法以及限流实践

1.固定时间窗口限流算法

        在固定时间内进行限流,比如下午1点到下午2点只允许用户进行操作十次,那如果超过这十次就会拒绝用户进行访问。

        问题:可能会出现流量突刺,如果用户正好卡在1点59分59秒进行十次访问,在2点00分00秒又进行了十次访问,那用户就在极短的时间内进行了多次访问,产生了流量突刺。

        简单进行实现固定窗口限流。

        统计限流次数使用常量即可。

import org.junit.jupiter.api.Test;

import java.time.LocalDateTime;
import java.time.ZoneOffset;

public class FixedTimeWindows {

    public static long recordTime = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) * 1000;

    /**
     * 获取限流的时间戳 毫秒 十秒内只能访问五次
     */
    public static final long LIMIT_TIME = LocalDateTime.now().plusSeconds(10).toEpochSecond(ZoneOffset.UTC) * 1000 - recordTime;

    /**
     * 定义一小时能可以进行调用的次数
     */
    public static final int LIMIT_TIMES = 5;

    /**
     * 存储用户调用的次数
     */
    public static int userTimes = 0;

    /**
     * 实现固定窗口限流算法
     */
    public static boolean fixedTimeWindowLimit() {
        // 获取当前时间
        long now = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) * 1000;

        if (now - recordTime > LIMIT_TIME) {
            // 更新时间限制
            System.out.println("更新时间限制");
            userTimes = 0;
            recordTime = now;
        } else {
            if (userTimes >= LIMIT_TIMES) {
                return false;
            }
        }
        userTimes++;
        return true;
    }

    public static void getNiuma() {
        System.out.println("牛马");
    }

    @Test
    void testGetNiuma() throws InterruptedException {

        for (int i = 0; i <= 20; i++) {
            Thread.sleep(1 * 1000);
            boolean result = fixedTimeWindowLimit();
            if (!result) {
                System.out.println("超出调用限制");
            } else {
                getNiuma();
            }
        }
    }
}

2.滑动时间窗口限流算法

        设定一个单位时间:一小时内只能有十次访问,在黄色期间的一小时只能进行访问十次,当时间发生变化向后推迟了十分中,在紫色范围能只能访问十次。

        这种算法巧妙的避免了流量突刺的情况,即使你分两段对我进行多次访问,只要这两段访问时间在我的单位时间单元内,那我就可以使用流量阻塞,不让你进行访问。

        优点:可以解决上述流量突刺的问题。

        缺点:实现相对复杂,限流效果和你的滑动单位有关,滑动单位越小,限流效果越好,但是往往很难进行选择一个特别合适的滑动单位。而且会直接暴力拒绝请求,比较影响用户的体验性。

        简单实现滑动窗口限流。

        统计限流次数使用数组进行滑动时间处理。

import org.junit.jupiter.api.Test;

import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Arrays;

public class SlideTimeWindow {

    /**
     * 定义初始化记录的事件
     */
    public static long recordTime = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) * 1000;

    /**
     * 获取限流的时间戳 毫秒 十秒内内只能访问五次
     */
    public static final long LIMIT_TIME = LocalDateTime.now().plusSeconds(10).toEpochSecond(ZoneOffset.UTC) * 1000 - recordTime;

    /**
     * 分割为十个时间单位
     */
    public static final int UNITS_OF_TIME = 10;

    /**
     * 定义一小时能可以进行调用的次数
     */
    public static final int LIMIT_TIMES = 5;

    /**
     * 存储用户调用的次数
     */
    public static int[] userTimeArr = new int[UNITS_OF_TIME];

    /**
     * 数组求和
     */
    public static int arrSum() {
        return Arrays.stream(userTimeArr).sum();
    }

    /**
     * 根据时间进行数组清零
     * @param number 时间
     */
    public static void arrCleanZero(int number) {
        for (int i = 0; i <= number; i++) {
            userTimeArr[i] = 0;
        }
    }

    /**
     * 定义滑动窗框限流
     */
    public static boolean slideTimeWindowLimit() {
        // 获取当前时间
        long now = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) * 1000;

        // 1. 当前时间 - 上次调用记录时间 > 10秒 清零 -> 允许调用
        if (now - recordTime >= LIMIT_TIME) {
            userTimeArr = new int[UNITS_OF_TIME];
        }

        // 2. 当前时间 - 上次调用记录时间 < 10秒
        if (now - recordTime < LIMIT_TIME) {
            // 2.1 数组求和 > 限制次数 滚蛋
            if (arrSum() >= LIMIT_TIMES) {
                // 一定要更新记录时间
                recordTime = now;
                return false;
            }
            // 2.2 数组求和 < 限制次数 允许调用
        }
        // 3. 调用规整

        // 3.0 给哪个时间加这个次数
        int timeNumber = (int) Math.floor((double) (now - recordTime) / 1000);
        // 处理过界情况
        if (timeNumber > 10) {
            timeNumber -= 10;
        }
        
        // 3.1 统一进行加1
        userTimeArr[timeNumber] += 1;
        // 3.2 更新记录时间
        recordTime = now;

        return true;
    }

    public static void getNiuma() {
        System.out.println("牛马");
    }

    @Test
    void testGetNiuma() throws InterruptedException {
        boolean result;

        for (int i = 0; i <= 10; i++) {
            Thread.sleep(500);
            result = slideTimeWindowLimit();
            if (!result) {
                System.out.println("超出调用限制");
            } else {
                getNiuma();
            }
        }

        Thread.sleep(5000);

        for (int i = 0; i <= 10; i++) {
            Thread.sleep(500);
            result = slideTimeWindowLimit();
            if (!result) {
                System.out.println("超出调用限制");
            } else {
                getNiuma();
            }
        }

        Thread.sleep(10000);

        for (int i = 0; i <= 10; i++) {
            Thread.sleep(500);
            result = slideTimeWindowLimit();
            if (!result) {
                System.out.println("超出调用限制");
            } else {
                getNiuma();
            }
        }
    }
}

3.漏桶限流算法

        漏桶算法就是定义一个桶,进行存储请求流量,但是出桶进行处理请求的时候,是根据固定速率出来请求进行处理。

        桶有一定的容量,如果请求存储到漏桶中,超出了容量,就会进行拒绝请求。

        优点:在一定程度上可以应对流量突刺,能够固定速率处理请求,保证服务器的安全。

        缺点:无法迅速处理一批请求,只能一个一个按顺序来处理(固定速率的缺点),当系统出现流量高峰的时候,还这样处理速度不变,对用户的体验会非常不好。

        简单实现漏桶限流算法。

import java.util.ArrayList;
import java.util.List;

public class LeakyBuckets {

    /**
     * 定义固定速率
     */
    public static final int SPEED = 1;

    /**
     * 定义桶容量
     */
    public static final int CAPACITY = 10;

    /**
     * 定义桶
     */
    public static List<Integer> bucketArr = new ArrayList<>();

    /**
     * 定义漏桶限流
     * false 执行限流 true 不执行限流
     * @return
     */
    public static boolean leakyBucketsLimit() {
        // 1. 将请求加入到漏桶中
        // 判断桶是否满了
        if (bucketArr.size() >= CAPACITY) {
            // 执行限流
            System.out.println("============== 桶满啦 ==============");
            return false;
        }
        // 实际过程中可以进行加入请求标识等
        bucketArr.add(1);

        // 2. 以固定流速进行处理请求
        // 这里只进行实现单线程的 实际请求中固定速率高的时候应该开多线程
        return true;
    }

    public static void getNiuma() {
        System.out.println("牛马");
    }
}

4.令牌桶限流算法

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

        实现令牌桶算法。

import java.util.concurrent.Callable;

public class MyCallable implements Callable<String> {

    private String name;

    public MyCallable(String name) {
        this.name = name;
    }

    @Override
    public String call() throws Exception {
        return TokenBucket.getNiuma(name);
    }
}
import org.junit.jupiter.api.Test;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class TokenBucket {

    /**
     * 定义固定速率
     */
    public static final int SPEED = 1;

    /**
     * 定义令牌前缀
     */
    public static final String TOKEN_FRONT = "token";

    /**
     * 定义令牌桶
     */
    public static List<String> tokenBucket = new ArrayList<>();

    /**
     * 生成令牌
     * @param key 关键key
     */
    public static String productToken(String key) {
        String token = TOKEN_FRONT + key;
        tokenBucket.add(token);
        return token;
    }


    /**
     * 初始化令牌
     * @param number 初始化令牌数量
     */
    public static void initToken(int number) {
        for (int i = 0; i < number; i++) {
            productToken("niuma" + i);
        }
    }

    /**
     * 校验令牌
     * @param token 令牌
     * @return false 限流 true 成功
     */
    public static boolean tokenBucketLimit(String token) {
        // 校验无令牌
        if (token == null || token.isEmpty() || token.equals(" ") || !tokenBucket.contains(token)) {
            return false;
        }

        return true;
    }

    public static String getNiuma(String name) {
        return "线程名: " + name + ", 牛马。";
    }

    @Test
    void testGetNiuma() throws ExecutionException, InterruptedException {
        // 初始化令牌
        initToken(3);

        // 初始化多线程
        MyCallable callable1 = new MyCallable("线程1");
        MyCallable callable2 = new MyCallable("线程2");
        FutureTask<String> futureTask1 = new FutureTask<>(callable1);
        FutureTask<String> futureTask2 = new FutureTask<>(callable2);

        // 校验令牌
        if (tokenBucketLimit(TOKEN_FRONT + "niuma1")) {
            new Thread(futureTask1).start();
            new Thread(futureTask2).start();
            System.out.println(futureTask1.get());
            System.out.println(futureTask2.get());
        }

        if (tokenBucketLimit("duaisbdiasu")) {
            new Thread(futureTask2).start();
            System.out.println(futureTask2.get());
        }
    }
}

        优点:能够并发处理同时的请求,并发性能会更高,因为在低流量的时候可以进行积攒令牌,高流量的时候,积攒了足够的令牌可以应对高并发的请求,解决了漏桶算法的问题。

        需要考虑的问题:还是存在时间单位选取的问题。如果流量突刺时令牌数量超过了系统可以进行接收的流量最大范围,那服务器就挂了,如果太少就会拒绝请求,导致用户体验过差。

5.限流粒度

        1.针对某个方法进行限流,即单位时间内最多允许同时XX个操作使用这个办法。

        2.针对某个用户进行限流,比如单个用户单位时间内最多执行XX次操作。

        3.针对某个用户X方法进行限流,比如单个用户时间内最多执行XX次这个方法。

6.四种限流算法的思考

        对于针对用户限流最大的问题就是,如果此时DDOS攻击者调动大量肉鸡,对我们的系统进行进攻,我们的限流如果仅仅针对用户的调用次数进行限流,并采用对用户进行限流的算法,那么我们的系统会被大量肉鸡产生的流量突刺,会占用大量服务器资源,直接突破我们的系统。

        所以我们就是不仅要对用户在操作接口时进行限流操作,更要在对每个接口进行限流。

        对于每个用户的限流就是为了防止DDOS攻击者操控一个用户大量进行请求,占用我们的资源,使得其他用户无法进行张昌访问。

        对于接口(全部用户)进行限流是为了防止接口遭遇流量突刺,因为如果我们仅仅对一个用户进行限流,DDOS攻击者,操控多个用户对我们发起突刺,也会沾满我们的系统,所以我们要对整个系统的接口进行限流。

        而且我们可以进行监控用户的操作,如果用户的操作,并非正常行为(比如持续高输出)那我们可以立刻对其进行封锁,限制用户行为,监控的话,我们可以使用AOP(单机)或者网关(分布式)。

7.本地限流实现(单机限流)

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

        使用Guava RateLimiter实现单机限流。

        底层使用的是令牌桶算法。

8.分布式限流实现(多机限流)

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

        1.把用户的使用频率等数据放到一个集中的存储中进行统计,比如Redis,这样无论用户的请求落到了哪台服务器,都可以以集中的数据存储内的数据为准(Redisson)。

        2.在网关中集中进行限流和统计(比如Sentinel,Spring Cloud Gateway)。

        redisson底层进行限流的时候使用的也是令牌桶算法进行限流。

9.使用redisson实现限流

9.1Redisson限流实现

        Redission内置了一个限流工具类,可以帮助你利用Redis来存储,来统计。

9.2Redisson的实现

9.2.1本地/远程安装Redis

        Redission依赖于Redis所以需要进行安装Redis进行使用。

9.2.2引入Redisson的依赖

<!-- https://github.com/redisson/redisson#quick-start -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.21.3</version>
</dependency>

9.2.3实现Redisson的配置

import lombok.Data;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConfigurationProperties(prefix = "spring.redis")
@Data
public class RedissonConfig {

    private Integer database;

    private String host;

    private Integer port;

    private String username;

    private String password;

    @Bean
    public RedissonClient getRedissonClient() {
        Config config = new Config();
        // 设置使用redis单机 设置数据库和地址
        config.useSingleServer()
                .setDatabase(database)
                .setAddress("redis://" + host + ":" + port);
        RedissonClient redissonClient = Redisson.create(config);
        return redissonClient;
    }
}

9.2.4实现限流操作

import com.yang.yangbi.common.ErrorCode;
import com.yang.yangbi.exception.BusinessException;
import org.redisson.api.RRateLimiter;
import org.redisson.api.RateIntervalUnit;
import org.redisson.api.RateType;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
public class RedisLimitManager {

    @Resource
    private RedissonClient redissonClient;

    /**
     * 限流操作
     *
     * @param key 区分不同的限流器
     */
    public void doRateLimit(String key) {
        // 创建一个名称为user_limiter的限流器, 每秒最多访问两次
        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);
        }
    }
}

9.2.5进行单元测试

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;

@SpringBootTest
class RedisLimitManagerTest {

    @Resource
    private RedisLimitManager redisLimitManager;

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

10.结语

        没想到自己居然听了一天白羊,麻了....

  • 29
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值