当前位置: 主页 > JAVA语言

漏桶算法 java实现-java实现旋转门算法

发布时间:2023-05-23 10:10   浏览次数:次   作者:佚名

什么是接口限流

那么什么是限流呢?顾名思义,限流就是限制流量,包括并发的流量和一定时间内的总流量,就像你宽带包了1个G的流量,用完了就没了,所以控制你的使用频率和单次使用的总消耗。

通过限流,我们可以很好地控制系统的qps,从而达到保护系统或者接口服务器稳定的目的。

接口限流的常用算法

计数器法

计数器法是限流算法里最简单也是最容易实现的一种算法。

比如我们规定,对于A接口来说,我们1分钟的访问次数不能超过100个。那么我们可以这么做:在一开始的时候,我们可以设置一个计数器counter,每当一个请求过来的时候,counter就加1,如果counter的值大于100并且该请求与第一个请求的间隔时间还在1分钟之内,那么说明请求数过多;

如果该请求与第一个请求的间隔时间大于1分钟,且counter的值还在限流范围内,那么就重置counter。

漏桶算法 java实现_pagerank算法java实现_java实现旋转门算法

具体算法的示意图如下:

java实现旋转门算法_漏桶算法 java实现_pagerank算法java实现

代码如下

class CounterDemo{
    private $first_request_time;
    private $request_count = 0; //已请求的次数
    public $limit = 100; //时间窗口内的最大请求数
    public $interval = 60; //时间窗口 s
    public function __construct() {
        $this->first_request_time = time();
    }
    public function grant(){
        $now = time();
        if($now < $this->first_request_time + $this->interval){
            //时间窗口内
            if($this->request_count < $this->limit) {
                $this->request_count++;
                return true;
            }else{
                return false;
            }
        }else{
            //超出前一个时间窗口后, 重置第一次请求时间和请求总次数
            $this->first_request_time = $now;
            $this->request_count = 1;
            return true;
        }
    }
}
$m = new CounterDemo();
$n_success = 0;
for($i=0; $i < 200; $i++){
    $rt = $m->grant();
    if($rt){
        $n_success ++;
    }
}
echo '成功请求 '.$n_success.' 次';

计数器算法很简单,但是有个严重的bug:

pagerank算法java实现_漏桶算法 java实现_java实现旋转门算法

一个恶意用户在0:59时瞬间发送了100个请求,然后再1:00时又瞬间发送了100个请求,那么这个用户在2秒内发送了200个请求。

上面我们规定1分钟最多处理100个请求, 也就是每秒1.7个请求。用户通过在时间窗口的重置节点处突发请求, 可以瞬间超过系统的承载能力,导致系统挂起或宕机。

java实现旋转门算法_漏桶算法 java实现_pagerank算法java实现

上面的问题,其实是因为我们统计的精度太低造成的。那么如何很好地处理这个问题呢?或者说,如何将临界问题的影响降低呢?我们可以看下面的滑动窗口算法。

java实现旋转门算法_漏桶算法 java实现_pagerank算法java实现

上图中,我们把一个时间窗口(一分钟)分成6份,每份(小格)代表10秒。每过10秒钟我们就把时间窗口往右滑动一格, 每一个格子都有自己独立的计数器。

比如一个请求在0:35秒到达的时候,就会落在0:30-0:39这个区间,并将此区间的计数器加1。

从上图可以看出, 0:59到达的100个请求会落在0:50-0:59这个灰色的格子中, 而1:00到达的100个请求会落在黄色的格子中。

而在1:00时间统计时, 窗口会往右移动一格,那么此时的时间窗口内的请求数量一共是200个漏桶算法 java实现,超出了限制的100个,触发了限流,后面的100个请求被抛弃或者等待。

如果我们把窗口时间划分越多, 比如60格,每格1s, 那么限流统计会更精确。

漏桶算法 (Leaky Bucket)

java实现旋转门算法_漏桶算法 java实现_pagerank算法java实现

漏桶算法(Leaky Bucket): 平滑网络上的突发流量。使其整流为一个稳定的流量。

java实现旋转门算法_pagerank算法java实现_漏桶算法 java实现

pagerank算法java实现_java实现旋转门算法_漏桶算法 java实现

java实现旋转门算法_pagerank算法java实现_漏桶算法 java实现

有一个固定容量的桶,有水流进来,也有水流出 去。对于流进来的水来说,我们无法预计一共有多少水会流进来,也无法预计水流的速度。但是对于流出去的水来说,这个桶可以固定水流出的速率。当桶满了之后,多余的水将会溢出(多余的请求会被丢弃)。

简单的算法实现:

class LeakyBucketDemo{
    private $last_req_time; //上一次请求的时间
    public $capacity; //桶的容量
    public $rate; //水漏出的速度(个/秒)
    public $water; //当前水量(当前累积请求数)
    public function __construct(){
        $this->last_req_time = time();
        $this->capacity = 100;
        $this->rate = 20;
        $this->water = 0;
    }
    public function grant(){
        $now = time();
        $water = max(0,$this->water - ($now - $this->last_req_time) * $this->rate);// 先执行漏水,计算剩余水量
        $this->water = $water;
        $this->last_req_time = $now;
        if($water < $this->capacity){
            // 尝试加水,并且水还未满
            $this->water += 1;
            return true;
        }else{
            // 水满,拒绝加水
            return false;
        }
    }
}
$m = new LeakyBucketDemo();
$n_success = 0;
for($i=0; $i < 500; $i++){
    $rt = $m->grant();
    if($rt){
        $n_success ++;
    }
    if($i > 0 && $i % 100 == 0){//每发起100次后暂停1s
        echo '已发送',$i,', 成功 ', $n_success,', sleep'.PHP_EOL;
        sleep(1);
    }
}
echo '成功请求 '.$n_success.' 次';

令牌桶算法 (Token Bucket)

pagerank算法java实现_java实现旋转门算法_漏桶算法 java实现

令牌桶算法比漏桶算法稍显复杂。首先,我们有一个固定容量的桶,桶里存放着令牌(token)。桶一开始是空的(可用token数为0),token以一个固定的速率r往桶里填充,直到达到桶的容量,多余的令牌将会被丢弃。每当一个请求过来时,就会尝试从桶里移除一个令牌,如果没有令牌的话漏桶算法 java实现,请求无法通过。

代码实现如下:

class TokenBucketDemo{
    private $last_req_time; //上次请求时间
    public $capacity; //桶的容量
    public $rate; //令牌放入的速度(个/秒)
    public $tokens; //当前可用令牌的数量
    public function __construct(){
        $this->last_req_time = time();
        $this->capacity = 100;
        $this->rate = 20;
        $this->tokens = 100; //开始给100个令牌
    }
    public function grant(){
        $now = time();
        $tokens = min($this->capacity,$this->tokens + ($now - $this->last_req_time) * $this->rate);// 计算桶里可用的令牌数
        $this->tokens = $tokens;
        $this->last_req_time = $now;
        if($this->tokens < 1){
            // 若剩余不到1个令牌,则拒绝
            return false;
        }else{
            // 还有令牌,领取1个令牌
            $this->tokens -= 1;
            return true;
        }
    }
}
$m = new TokenBucketDemo();
$n_success = 0;
for($i=0; $i < 500; $i++){
    $rt = $m->grant();
    if($rt){
        $n_success ++;
    }
    if($i > 0 && $i % 100 == 0){//每发起100次后暂停1s
        echo '已发送',$i,', 成功 ', $n_success,', sleep'.PHP_EOL;
        sleep(1);
    }
}
echo '成功请求 '.$n_success.' 次';

我们可以使用redis的队列作为令牌桶容器使用,使用lPush(入队),rPop(出队),实现令牌加入与消耗的操作。

TokenBucket.php

_config = $config;
        $this->_queue = $queue;
        $this->_max = $max;
        $this->_redis = $this->connect();
    }
    /**
     * 加入令牌
     * @param  Int $num 加入的令牌数量
     * @return Int 加入的数量
     */
    public function add($num=0){
        // 当前剩余令牌数
        $curnum = intval($this->_redis->lSize($this->_queue));
        // 最大令牌数
        $maxnum = intval($this->_max);
        // 计算最大可加入的令牌数量,不能超过最大令牌数
        $num = $maxnum>=$curnum+$num? $num : $maxnum-$curnum;
        // 加入令牌
        if($num>0){
            $token = array_fill(0, $num, 1);
            $this->_redis->lPush($this->_queue, ...$token);
            return $num;
        }
        return 0;
    }
    /**
     * 获取令牌
     * @return Boolean
     */
    public function get(){
        return $this->_redis->rPop($this->_queue)? true : false;
    }
    /**
     * 重设令牌桶,填满令牌
     */
    public function reset(){
        $this->_redis->delete($this->_queue);
        $this->add($this->_max);
    }
    /**
     * 创建redis连接
     * @return Link
     */
    private function connect(){
        try{
            $redis = new Redis();
            $redis->connect($this->_config['host'],$this->_config['port'],$this->_config['timeout'],$this->_config['reserved'],$this->_config['retry_interval']);
            if(empty($this->_config['auth'])){
                $redis->auth($this->_config['auth']);
            }
            $redis->select($this->_config['index']);
        }catch(RedisException $e){
            throw new Exception($e->getMessage());
            return false;
        }
        return $redis;
    }
} 
?>

令牌的假如与消耗

 'localhost',
    'port' => 6379,
    'index' => 0,
    'auth' => '',
    'timeout' => 1,
    'reserved' => NULL,
    'retry_interval' => 100,
);
// 令牌桶容器
$queue = 'mycontainer';
// 最大令牌数
$max = 5;
// 创建TrafficShaper对象
$tokenBucket = new TokenBucket($config, $queue, $max);
// 重设令牌桶,填满令牌
$tokenBucket->reset();
// 循环获取令牌,令牌桶内只有5个令牌,因此最后3次获取失败
for($i=0; $i<8; $i++){
    var_dump($tokenBucket->get());
}
// 加入10个令牌,最大令牌为5,因此只能加入5个
$add_num = $tokenBucket->add(10);
var_dump($add_num);
// 循环获取令牌,令牌桶内只有5个令牌,因此最后1次获取失败
for($i=0; $i<6; $i++){
    var_dump($tokenBucket->get());
}
?>

漏桶算法 java实现_pagerank算法java实现_java实现旋转门算法

领取方式:点赞关注小编后私信【资料】获取资料领取方式!

部分资料展示:

java实现旋转门算法_pagerank算法java实现_漏桶算法 java实现

java实现旋转门算法_漏桶算法 java实现_pagerank算法java实现

pagerank算法java实现_java实现旋转门算法_漏桶算法 java实现

pagerank算法java实现_java实现旋转门算法_漏桶算法 java实现

pagerank算法java实现_java实现旋转门算法_漏桶算法 java实现

领取方式:点赞关注小编后私信【资料】获取资料领取方式!