因为现在动不动就说高并发,说到高并发 就不得不提并发下限流、熔断、降级。
为什么要进行接口限流呢?
个人认为其实目的都是为了保证线上系统的稳定性,防止因为高频访问服务器而导致服务器宕机。
下面来简单实现一下接口限流的常用算法
1.使用计数器进行限流
这应该是最简单也是最容易实现的,比如A接口1分钟内的访问次数不能超过100个。那么可以这么做:在一开始的时候,设置一个计数器counter,每当一个请求过来的时候,counter就加1,如果counter的值大于100并且该请求与第一个请求的间隔时间还在1分钟之内,那么说明请求数过多;如果该请求与第一个请求的间隔时间大于1分钟,就重置计数器。为了保证高并发下的原子性使用redis的incr实现计数器限流。
public function __construct()
{
$this->redis = new \Redis();
$this->redis->connect('127.0.0.1', 6379);
}
public function getApi()
{
//计数器限流
//$res = $this->SpeedCounter();
//滑动窗口算法
//$res = $this->SlideTimeWindow();
//漏斗算法
$res = $this->leakBucket();
$data = [
'msg' => '获取成功',
'code' => 200
];
if (!$res) {
$data['msg'] = '请稍后重试';
$data['code'] = 400;
}
return $data;
}
/**
* @Notes: redis 使用计数器进行限流
* @Author:如果,
* @Date: 2022/9/16,
* @Time: 20:05,
* @return bool
*/
public function SpeedCounter()
{
$redis = $this->redis;
//时间
$limitTime = 60;
//最大请求次数
$maxCount = 10;
$redisKey = "userRequestNum";
$count = $redis->incr($redisKey);
print_r($count);
//换行
echo PHP_EOL;
if ($count == 1) {
$redis->expire($redisKey, $limitTime);
}
if ($count > $maxCount) {
return false;
}
return true;
}
/**
* @Notes: redis 使用计数器进行限流
* @Author:如果,
* @Date: 2022/9/16,
* @Time: 20:05,
* @return bool
*/
public function SpeedCounter()
{
$redis = $this->redis;
//时间
$limitTime = 60;
//最大请求次数
$maxCount = 10;
$redisKey = "userRequestNum";
$count = $redis->incr($redisKey);
print_r($count);
//换行
echo PHP_EOL;
if ($count == 1) {
$redis->expire($redisKey, $limitTime);
}
if ($count > $maxCount) {
return false;
}
return true;
}
手动执行php文件 前面10次都可以访问后面10次我们主动拦截请求 粗略的看是实行了限流的方法。这方法存在的问题就是最后1秒内涌入所有请求,然后计数器过期重置后第一秒内又涌入大量请求 这样服务器还是可能会被高频访问搞挂。为了解决这种方法又出现了滑动窗口算法
2.滑动窗口算法
百度拿的图,滑动窗口个人认为其实就是多存了时间,每次请求进来后时间范围之外的数据将被动态删除。主要使用redis的zset结构来实现
/**
* @Notes:滑动窗口算法
* @Author:如果,
* @Date: 2022/9/16,
* @Time: 20:20,
*/
public function SlideTimeWindow()
{
/**
* 2.redis 滑动窗口实现方式
* 限制1分钟内最大只能请求10次
* 使用redis事务保证redis原子性
*/
$redis = $this->redis;
$limitTime = 60;
$maxCount = 10;
$redisKey = "slide_api";
$nowTime = time();
//使用管道提升性能 pipe
$pipe = $redis->multi();
//value 和 score 都使用时间戳,因为相同的元素会覆盖
$pipe->zAdd($redisKey, $nowTime, $nowTime);
//移除时间窗口之前的行为记录,剩下的都是时间窗口内的
$pipe->zRemRangeByScore($redisKey, 0, $nowTime - $limitTime);
//查看redis里还有多少次
$pipe->zCard($redisKey);
$pipe->expire($redisKey, 60 + 1);
$replies = $pipe->exec();
//有限时间窗口内的数量超过限制返回 true:false
return $replies[2] <= $maxCount;
}
主要就是根据时间判断总体思路和计数器差不多
然后来执行一下查看效果
可以看到10次之后就拦截了请求。
3.漏斗算法
顾名思义漏斗,其实就是处理的速度是一定的。主要目的是控制数据注入到网络的速率,平滑网络上的突发流量。漏斗算法提供了一种机制,通过它,突发流量可以被整形以便为网络提供一个稳定的流量。但是多余的请求将会被直接丢弃。 我更觉得是将流量控制在自己可以承受范围内,自己控制流量的速度。 处理请求的worker以固定的速度从桶中取出请求进行处理。 如果桶满了,直接返回请求频率超限的错误码或者页面 限流 体现在worker从桶中取请求的速度上
流量最均衡的限流实现方式。nginx的limit模块就是使用了漏斗算法
private $_water; //漏斗的当前水量(也就是请求数)
private $_burst = 10; //漏斗总量(超过将直接舍弃)
private $_rate = 1; //漏斗出水速率(限流速度)
private $_lastTime; //记录每次请求的时间(因为需要记录每次请求之间的出水量也就是请求数)
/**
* @Notes:漏斗算法
* @Author:如果,
* @Date: 2022/9/16,
* @Time: 21:03,
*/
public function leakBucket()
{
$nowTime = time();
$redisKey = "leakBucket_api";
if (!empty($time = $this->redis->get($redisKey))) {
$this->_lastTime = $time; //获取上一次访问时间
}
if (!empty($water = $this->redis->get('water'))) {
$this->_water = $water;//获取当前剩余量
}
$s = $nowTime - $this->_lastTime; //请求间隔
$outCount = $s * $this->_rate;//请求间隔 * 出水速度 (着段时间应该出多少滴水)
//桶里的水去 - 这段时间要出的水= 桶里剩余的水
$this->_water = ($this->_water - $outCount);
//桶里剩余的水为 -数
if ($this->_water <= 0) {
$this->_water = 0; //漏斗重新赋值为空
}
if ($this->_water > $this->_burst) {
echo "超出桶限制" . PHP_EOL;
return false;
}
print_r($this->_water);
//重新赋值时间 下次请求这个时间 就为上次时间
$this->redis->set($redisKey, $nowTime);
//把桶里剩余的水 重新赋值到redis里 并+1 本次也算请求
$this->redis->set('water', $this->_water + 1);
}
不足之处在于:
面对突发流量时会有大量请求失败,但是我们可以使用预先准备好的资源返回。