因为现在动不动就说高并发,说到高并发 就不得不提并发下限流、熔断、降级。
为什么要进行接口限流呢?
个人认为其实目的都是为了保证线上系统的稳定性,防止因为高频访问服务器而导致服务器宕机。
下面来简单实现一下接口限流的常用算法:
1.使用计数器进行限流
这应该是最简单也是最容易实现的,比如A接口1分钟内的访问次数不能超过100个。那么可以这么做:在一开始的时候,设置一个计数器counter,每当一个请求过来的时候,counter就加1,如果counter的值大于100并且该请求与第一个请求的间隔时间还在1分钟之内,那么说明请求数过多;如果该请求与第一个请求的间隔时间大于1分钟,就重置计数器。为了保证高并发下的原子性使用redis的incr实现计数器限流。
speedCounterApi.php文件
<?php
//计数器实现限流
class speedCounterApi
{
//要访问的接口
public function getApi()
{
$res = (new speedCounterApi())->SpeedCounter();
$data = [
'msg' => '获取成功',
'code' => 200
];
if (!$res) {
$data['msg'] = '请稍后重试';
$data['code'] = 400;
}
return $data;
}
/**
* 1.redis 计数器实现方式
* 限制1分钟内最大只能请求10次
*/
public function SpeedCounter()
{
$redis = new \Redis();
$redis->connect('10.225.137.105', 6379);
$limitTime = 60;
// 最大请求数量
$maxCount = 10;
$redisKey = 'api';
// incr命令
// 为键 key 储存的数字值加上一。
// 如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作。
$count = $redis->incr($redisKey);
print_r($count);
echo PHP_EOL;
//设置计算器的时间
if ($count == 1) {
//设置 Key -/s的过期时间
$redis->expire($redisKey, $limitTime);
}
//1分钟内超过最大请求数
if ($count > $maxCount) {
return false;
}
return true;
}
}
$res = (new speedCounterApi())->getApi();
print_r($res);
echo PHP_EOL;
然后来执行一下查看效果
手动执行php文件 前面10次都可以访问后面10次我们主动拦截请求 粗略的看是实行了限流的方法。这方法存在的问题就是最后1秒内涌入所有请求,然后计数器过期重置后第一秒内又涌入大量请求 这样服务器还是可能会被高频访问搞挂。为了解决这种方法又出现了滑动窗口算法
2.滑动窗口算法
百度拿的图,滑动窗口个人认为其实就是多存了时间,每次请求进来后时间范围之外的数据将被动态删除。主要使用redis的zset结构来实现
slideTimeWindow.php文件
<?php
//2.滑动窗口实现
class SlideTimeWindowApi
{
//要访问的接口
public function getApi()
{
$res = (new SlideTimeWindowApi())->SlideTimeWindow();
$data = [
'msg' => '获取成功',
'code' => 200
];
if (!$res) {
$data['msg'] = '请稍后重试';
$data['code'] = 400;
}
return $data;
}
/**
* 2.redis 滑动窗口实现方式
* 限制1分钟内最大只能请求10次
* 使用redis事务保证redis原子性
*/
public function SlideTimeWindow()
{
$redis = new \Redis();
$redis->connect('10.225.137.105', 6379);
$limitTime = 60;
// 最大请求数量
$maxCount = 10;
$redisKey = 'slide_api';
$nowTime = time();
//使用管道提升性能
$pipe = $redis->multi();
//value 和 score 都使用时间戳,因为相同的元素会覆盖
$sss=$pipe->zadd($redisKey, $nowTime, $nowTime);
//移除时间窗口之前的行为记录,剩下的都是时间窗口内的
$pipe->zremrangebyscore($redisKey, 0, $nowTime - $limitTime);
//获取窗口内的行为数量
$pipe->zcard($redisKey);
//多加一秒过期时间
$pipe->expire($redisKey, 60 + 1);
//执行
$replies = $pipe->exec();
var_dump($replies[2]);
//判断在有限时间窗口内的数量是否超过限制 true:false
return $replies[2] <= $maxCount;
}
}
$res = (new SlideTimeWindowApi())->getApi();
print_r($res);
echo PHP_EOL;
主要就是根据时间判断总体思路和计数器差不多
然后来执行一下查看效果
可以看到10次之后就拦截了请求。
3.漏斗算法
顾名思义漏斗,其实就是处理的速度是一定的。主要目的是控制数据注入到网络的速率,平滑网络上的突发流量。漏斗算法提供了一种机制,通过它,突发流量可以被整形以便为网络提供一个稳定的流量。但是多余的请求将会被直接丢弃。
我更觉得是将流量控制在自己可以承受范围内,自己控制流量的速度。
处理请求的worker以固定的速度从桶中取出请求进行处理。
如果桶满了,直接返回请求频率超限的错误码或者页面
限流 体现在worker从桶中取请求的速度上
流量最均衡的限流实现方式。nginx的limit模块就是使用了漏斗算法
leackBucketApi.php文件
<?php
/**
* 3.漏斗算法实现方式
*/
class leackBucketApi
{
private $_water; //漏斗的当前水量(也就是请求数)
private $_burst=2; //漏斗总量(超过将直接舍弃)
private $_rate = 1; //漏斗出水速率(限流速度)
private $_lastTime; //记录每次请求的时间(因为需要记录每次请求之间的出水量也就是请求数)
private $_redis; //redis对象
public function __construct()
{
$this->_redis = new \Redis();
$this->_redis->connect('10.225.137.105', 6379);
$this->_lastTime = time(); //需要记录每次访问的时间
}
//要访问的接口
public function getApi()
{
$res = (new leackBucketApi())->leackBucket();
$data = [
'msg' => '获取成功',
'code' => 200
];
if (!$res) {
$data['msg'] = '请稍后重试';
$data['code'] = 400;
}
return $data;
}
/**
*
*/
public function leackBucket()
{
$now = time();
$redisKey = 'leackBucket_api';
if (!empty($time = $this->_redis->get($redisKey))) {
$this->_lastTime = $time; //获取上一次访问时间
}
if (!empty($water = $this->_redis->get('water'))) {
$this->_water = $water;//获取当前剩余量也就是请求数
}
//计算出水量
//因为rate是固定的,所以可以认为“时间间隔 * rate”即为漏出的水量
$s=$now - $this->_lastTime; //当前时间减去上次访问时间,得到时间间隔
$outCount=$s * $this->_rate; //漏出的水量(请求数)
//执行漏水,计算剩余水量,也就是当前请求
$this->_water=($this->_water - $outCount);
if($this->_water<0){
$this->_water=0; //重置为0
}
var_dump($this->_water.'----请求数');
//请求数超出了突发请求限制
if($this->_water > $this->_burst){
echo "超出桶限制".PHP_EOL;
return false;
}
$this->_lastTime = $now; // 更新时间
$water++;//更新待请求数
$this->_redis->set($redisKey,$now);
//记录请求数
$this->_redis->set('water',$water);
return true;
}
}
$demo=new leackBucketApi();
//模拟请求
while (true){
$time=mt_rand(600000,3600000);
echo "'执行请求结果:".PHP_EOL;
var_dump($demo->getApi());
var_dump('随机时间:'.$time);
usleep($time); //随机时间
}
然后来执行一下查看效果
不足之处在于:
面对突发流量时会有大量请求失败,但是我们可以使用预先准备好的资源返回。
4.令牌桶算法
令牌桶算法在漏桶算法流量处理均衡的基础上,允许一定程度的突发流量。
适合电商抢购或者微博出现热点事件这种场景。
令牌桶算法是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌。
假设限制r/s,表示每秒会有r个令牌放入桶中,或者说每过1/r秒桶中增加一个令牌 桶中最多存放b个令牌,当桶满时,新添加的令牌被丢弃或拒绝 当一个n个字节大小的数据包到达,将从桶中删除n个令牌,接着数据包被发送到网络上 如果桶中的令牌不足n个,则不会删除令牌,且该数据包将被限流(要么丢弃,要么在缓冲区等待)
对于令牌桶中令牌的产生一般有两种做法:
1、开启一个定时任务,由定时任务持续生成令牌。这样的问题在于会极大的消耗系统资源,如,某接口需要分别对每个用户做访问频率限制,假设系统中 存在6W用户,则至多需要开启6W个定时任务来维持每个桶中的令牌数,这样的开销是巨大的。
2、在每次获取令牌之前计算,其实现思路为,根据时间来计算访问前后该段时间内可以生成多少令牌,将生成的令牌加入令牌桶中并 更新数据。这样一来,只需要在获取令牌时计算一次即可。
令牌桶的主要限流体现在放入令牌的速度。
tokenBucketApi.php文件
<?php
/**
* 4.令牌桶算法实现
*/
class tokenBucketApi
{
private $_lastTime; //记录每次请求的时间
private $_burst = 2; // 桶的容量
private $_rate = 1; // 令牌放入的速度
private $_tokens; // 当前令牌的数量
private $_redis; //redis对象
public function __construct()
{
$this->_redis = new \Redis();
$this->_redis->connect('10.225.137.105', 6379);
$this->_lastTime = time(); //需要记录每次访问的时间
}
//要访问的接口
public function getApi()
{
$res = (new tokenBucketApi())->tokenBucket();
$data = [
'msg' => '获取成功',
'code' => 200
];
if (!$res) {
$data['msg'] = '请稍后重试';
$data['code'] = 400;
}
return $data;
}
public function tokenBucket()
{
$now = time();
$redisKey = 'tokenBucket_api';
if (!empty($time = $this->_redis->get($redisKey))) {
$this->_lastTime = $time; //获取上一次访问时间
}
if (!empty($tokens = $this->_redis->get('tokens'))) {
$this->_tokens = $tokens;//获取当前令牌的数量
}
//计算生成的令牌量
//因为rate是固定的,所以可以认为“时间间隔 * rate”即为生成的令牌量
$s = $now - $this->_lastTime; //当前时间减去上次访问时间,得到时间间隔
//生成的令牌 =(当前时间-上次刷新时间)* 放入令牌的速率
$addTokens = $s * $this->_rate;
//当前令牌数= 之前的桶内令牌数量+放入的令牌数量(不能超过桶的总量)
$this->_tokens = min($this->_burst, $this->_tokens + $addTokens);
echo "令牌量:".$this->_tokens;
//桶里面还有令牌,请求正常处理
$this->_lastTime = $now;
$this->_redis->set($redisKey, $now);
if ($this->_tokens < 1) {
// 若不到1个令牌,则拒绝
echo "拿不到令牌".PHP_EOL;
return false;
}
// 还有令牌,领取令牌
$this->_tokens -= 1;
//记录请求数
$this->_redis->set('tokens', $this->_tokens);
return true;
}
}
$demo = new tokenBucketApi();
var_dump($demo->getApi());
//模拟请求
//while (true) {
// $time = mt_rand(600000, 3600000);
// echo "'执行请求结果:" . PHP_EOL;
// var_dump($demo->getApi());
//var_dump('随机时间:' . $time);
//usleep($time); //随机时间
}
然后手动执行多次
可以看到当慢慢执行的话 一直可以获取到令牌 因为令牌桶里一直有令牌,当请求过快将会丢弃(当然最好是返回我们预定好的资源)。过段时间令牌桶里又有了令牌又可以获取到令牌请求api。
放一张他们的对比图。