PHP基于Redis使用令牌桶算法实现限流

本文章主要介绍PHP基于Redis使用令牌桶算法实现限流!

相信大家都知道为什么要做限流,为了保证服务不中断,防止down机,就需要拒绝多余的请求。


例如:服务器只能抗1000qps,但这时来了很大的流量,导致服务器瞬间压力飙升,使系统超负荷工作。

我们在业务层面进行限流,减轻服务器压力。

令牌桶算法

令牌桶中有初始令牌数,每来一个请求从桶中获取一个令牌,并且在一定时间间隔中可以生成令牌,多余的令牌被丢弃。由此实现限流。

  1. 设置一个令牌桶,桶内存放令牌,初始令牌桶内的令牌是满的(桶内令牌的数量根据服务器情况设定)。
  2. 每来一个请求从桶中获取一个令牌,当未拿到令牌,则不允许再访问。
  3. 使用定时任务,定时投递令牌,最多使桶内令牌满额。

可以使用Redis的队列作为令牌桶使用,使用lPush(入队),rPop(出队),实现令牌投递和消费。
在这里插入图片描述
TokenBucket.php(令牌桶算法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
<?php

/**
* PHP基于Redis使用令牌桶算法实现限流
*/

class TokenBucket
{
/**
* 最大令牌数
* @var int
*/
private $max;

/**
* 令牌桶
* @var string
*/
private $queue;

/**
* redis 配置
*/
private $config;

/**
* redis 实例
* @var object
*/
private $redis;

/**
* 初始化
*/
public function __construct($max, $queue = 'myBucket', $config = [])
{
$this->max = $max;
$this->queue = $queue;
$this->config = $config ? $config : ['host' => '127.0.0.1', 'port' => 6379];
$this->redis = $this->connect();
}

/**
* redis 连接
*/
public function connect()
{
$redis = new Redis();
$redis->connect($this->config['host'], $this->config['port']);
return $redis;
}

/**
* 添加令牌 令牌上限
*/
public function add($num)
{
// 当前剩余令牌数
$currnum = intval($this->redis->llen($this->queue));

// 最大令牌数
$max = intval($this->max);

// 添加的令牌数(不可大于最大令牌数)
$num = $currnum + $num <= $max ? $num : $max - $currnum;

if ($num > 0) {
$token = array_fill(0,$num,1);
// 令牌入队列
$this->redis->lpush($this->queue, ...$token);
return true;
}
return false;
}

/**
* 获取令牌
*/
public function get()
{
return $this->redis->rpop($this->queue);
}

/**
* 初始化令牌,填满令牌桶
*/
public function reset()
{
$this->redis->del($this->queue);
$this->add($this->max);
}
}

demo.php(模拟用户请求消费令牌)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php

include 'TokenBucket.php';

use function Swoole\Coroutine\run;
use Swoole\Timer;

/**
* 模拟用户请求,消费令牌
*/
run(function(){
go(function(){
// 最大令牌数
$max = 100;

$TokenBucket = new TokenBucket($max);
// 重设令牌桶,填满令牌
$TokenBucket->reset();

// 使用swoole定时器,模拟用户请求,消费令牌
Timer::tick(100, function($timer_id) use ($TokenBucket){
if ($TokenBucket->get()) {
echo '['.date('Y-m-d H:i:s').'] ' . '请求成功!' . PHP_EOL;
// 执行业务
} else {
echo '['.date('Y-m-d H:i:s').'] ' . '请求频繁!' . PHP_EOL;
}
});
});
});

定时投递令牌

这里使用到swoole定时器,实现定时投递令牌!
deliver.php(模拟用户请求消费令牌)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php

include 'TokenBucket.php';

use function Swoole\Coroutine\run;
use Swoole\Timer;

/**
* swoole定时器投递令牌
*
* 定时器的时间和最大令牌数决定一秒接收多个请求,多余请求将拒绝。
*/
run(function(){
go(function(){

// 每次时间间隔加入的令牌数
$token_num = 10;

// 定时器指定时间
$msec = 1000;

$TokenBucket = new TokenBucket(100);

Timer::tick($msec, function($timer_id) use ($TokenBucket, $token_num){
// 投递令牌
$status = $TokenBucket->add($token_num) ? 'true' : 'fasle';
echo '['.date('Y-m-d H:i:s').'] ' . 'add token status: ' . $status . PHP_EOL;
});
});
});

测试

执行deliver.php,执行demo.php进行测试

1
php deliver.php
1
php demo.php

截图:
在这里插入图片描述)在这里插入图片描述
搞定!