限流算法の初体验

什么是限流算法

限流,也称流量控制。是指系统在面临高并发,或者大流量请求的情况下,限制新的请求对系统的访问,从而保证系统的稳定性。限流会导致部分用户请求处理不及时或者被拒,这就影响了用户体验。所以一般需要在系统稳定和用户体验之间平衡一下。

简单的例子:一些热门的旅游景区,一般会对每日的旅游参观人数有限制的。每天只会卖出固定数目的门票,比如1000张。假设在五一、国庆假期,你去晚了,可能当天的票就已经卖完了,就无法进去游玩了。即使你进去了,排队速度也会变得非常的缓慢。

为什么要使用限流算法

有时候特殊的日期比如双十一等节假日,请求访问会是平时的好多好多倍,如果不限流,服务器可能会顶不住压力崩溃;其次,限流还能阻止一些恶意的调用攻击,不让你一下子调用很多线程疯狂调用,对系统的安全都有很好的保护,所以限流算法是我们后端开发必须要学习的一块知识点.

常见的限流算法

1.固定窗口限流(计数器限流)

最容易实现的限流算法,假设系统能同时处理100个请求,保存一个计数器,处理了一个请求,计数器加一,一个请求处理完毕之后计数器减一。每次请求来的时候看看计数器的值,如果超过阈值要么拒绝。

这里对于计数器的要求是所有集群都能访问的,在单机模式下,可以直接使用Java里的AtomicInteger等原子类当计数器,在分布式的环境下,可以用redis中的incr命令来实现

在这里插入图片描述

优点:实现简单

缺点:无法应对突发的流量,比如我们允许的阈值是100万,此时计数器的值为0,100万个请求一下子全部涌入,服务器可能会直接崩溃 ,缓缓的增加处理和一下子涌入对于程序来说是不一样的。还存在临界值问题。
在这里插入图片描述

使用场景:单位时间内允许部分操作,比如1 小时只允许 10 个用户操作。

简单示例:

    /**
     * 固定窗口时间算法
     * @return
     */
    public boolean fixedWindowsTryAcquire() {
        long currentTime = System.currentTimeMillis();  //获取系统当前时间
        if (currentTime - lastRequestTime > windowUnit) {  //检查是否在时间窗口内
            counter = 0;  // 计数器清0
            lastRequestTime = currentTime;  //开启新的时间窗口
        }
        if (counter < threshold) {  // 小于阀值
            counter++;  //计数器加1
            return true;
        }

        return false;
    }

2.滑动窗口限流

滑动窗口限流像是固定窗口的优化方案,用来解决固定窗口临界值的问题。它将单位时间周期分为n个小周期,分别记录每个小周期内接口的访问次数,并且根据时间滑动删除过期的小周期。
在这里插入图片描述
单位时间内允许部分操作,但是这个单位时间是滑动的,需要指定一个滑动单位比如滑动单位
假设单位时间还是1s,滑动窗口算法把它划分为5个小周期,也就是滑动窗口(单位时间)被划分为5个小格子。每格表示0.2s。每过0.2s,时间窗口就会往右滑动一格。每个小周期,都有自己独立的计数器,如果请求是0.81s到达的,0.8~1.0s对应的计数器就会加1。

开始前:0s 1h 2h

一分钟后:1min 1h1min

优点:能够解决上述流量突刺的问题,因为第 59 分钟时,限流窗口是 59 分 ~ 1小时 59 分,这个时间段内只能接受 10 次请求,只要还在这个窗口内,更多的操作就会被拒绝。

缺点:实现相对复杂,限流效果和你的滑动单位有关,滑动单位越小,限流效果越好,但往往很难选取到一个特别合适的滑动单位。

使用场景:

  • 阿里的限流工具Sentinel使用的就是滑动窗口

简单示例:

/**
     * 单位时间划分的小周期(单位时间是1分钟,10s一个小格子窗口,一共6个格子)
     */
    private int SUB_CYCLE = 10;

    /**
     * 每分钟限流请求数
     */
    private int thresholdPerMin = 100;

    /**
     * 计数器, k-为当前窗口的开始时间值秒,value为当前窗口的计数
     */
    private final TreeMap<Long, Integer> counters = new TreeMap<>();

   /**
     * 滑动窗口时间算法实现
     */
    boolean slidingWindowsTryAcquire() {
        long currentWindowTime = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) / SUB_CYCLE * SUB_CYCLE; //获取当前时间在哪个小周期窗口
        int currentWindowNum = countCurrentWindow(currentWindowTime); //当前窗口总请求数

        //超过阀值限流
        if (currentWindowNum >= thresholdPerMin) {
            return false;
        }

        //计数器+1
        counters.get(currentWindowTime)++;
        return true;
    }

   /**
    * 统计当前窗口的请求数
    */
    private int countCurrentWindow(long currentWindowTime) {
        //计算窗口开始位置
        long startTime = currentWindowTime - SUB_CYCLE* (60s/SUB_CYCLE-1);
        int count = 0;

        //遍历存储的计数器
        Iterator<Map.Entry<Long, Integer>> iterator = counters.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<Long, Integer> entry = iterator.next();
            // 删除无效过期的子窗口计数器
            if (entry.getKey() < startTime) {
                iterator.remove();
            } else {
                //累加当前窗口的所有计数器之和
                count =count + entry.getValue();
            }
        }
        return count;
    }

3.漏桶算法

漏桶算法是一种基于水滴滴落的原理实现的限流算法,当用户请求被加入到漏桶中时,系统会根据漏桶的速率向用户发送请求,如果漏桶中水滴已经满,则拒绝用户请求。漏桶算法的优点是可以保证用户请求的平均速率不会超过限定的速率,缺点是需要消耗较多的CPU资源。

简单来说就是每秒处理 10 个请求,桶的容量是 10,每 0.1 秒固定处理一次请求,如果 1 秒内来了 10 个请求;都可以处理完,但如果 1 秒内来了 11 个请求,最后那个请求就会溢出桶,被拒绝。

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

代码实现:

public class LeakyBucket {
    private int rate; // 漏桶速率
    private int capacity; // 漏桶容量
    private int count; // 漏桶中水滴数量
    private long lastTime; // 上一次滴水时间

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

    public boolean check() {
        long now = System.currentTimeMillis();
        long elapsedTime = now - lastTime;
        lastTime = now;
        if (elapsedTime < rate) {
            // 等待一段时间后再滴水
            return false;
        } else {
            // 滴水,清空漏桶中的水滴
            count = 0;
            return true;
        }
    }
}

4.令牌桶算法

令牌桶算法是一种基于令牌掉落的原理实现的限流算法,当用户请求被加入到令牌桶中时,系统会根据令牌桶的速率向用户发送请求,如果令牌桶中令牌已经用完,则拒绝用户请求。令牌桶算法的优点是可以保证用户请求的平均速率不会超过限定的速率,缺点是需要消耗较多的CPU资源。

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

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

代码示例:

public class TokenBucket {
    private int rate; // 令牌桶速率
    private int capacity; // 令牌桶容量
    private int tokens; // 令牌桶中令牌数量
    private long lastTime; // 上一次掉令牌时间

    public TokenBucket(int rate, int capacity) {
        this.rate = rate;
        this.capacity = capacity;
        this.tokens = capacity;
        this.lastTime = System.currentTimeMillis();
    }

    public boolean check() {
        long now = System.currentTimeMillis();
        long elapsedTime = now - lastTime;
        lastTime = now;
        if (elapsedTime < rate) {
            // 等待一段时间后再掉令牌
            return false;
        } else {
            // 掉令牌,更新令牌数量和时间戳
            tokens--;
            return true;
        }
    }
}

限流粒度的考虑

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

几种常用的限流实现类库

1)本地限流(单机限流)每个服务器单独限流,一般适用于单体项目,就是你的 项目只有一个服务器 。
Guava RateLimiter:

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

public static void main(String[] args) {
	// 每秒限流5个请求
	RateLimiter limiter = RateLimiter.create(5.0);
	while (true) {
		if (limiter.tryAcquire()) {
		// 处理请求
		}else {// 超过流量限制,需要做何处理
		}
	}
}

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

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

Redission限流示例代码:

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();
	// 获取限流器
	RSemaphore semaphore = redisson.getSemaphore("mySemaphore");
	// 尝试获取许可证
	boolean result = semaphore.tryAcquire();
	if (result) {
		// 处理请求
	} else {
		// 超过流量限制,需要做何处理
	}
}

搭建自己的限流工具类(RedisLimiterManager)

什么是 Manager?专门提供 RedisLimiter 限流基础服务的( 提供了通用的能力,可以放到任何一个项目里 )

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

	/*** 限流操作** @param key 区分不同的限流器,比如不同的用户 id 应该分别统计*/
	public void doRateLimit(String key) {
		// 创建一个名称为user_limiter的限流器,每秒最多访问 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);
		}
	}
}

应用到要限流的方法中:

User loginUser = userService.getLoginUser(request);
// 限流判断,每个用户一个限流器
redisLimiterManager.doRateLimit("genChartByAi_" + loginUser.getId());
// 后续操作xxxxx

参考文章:https://juejin.cn/post/6967742960540581918

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值