限流算法
计数器
采用计数器实现限流有点简单粗暴,一般我们会限制一秒钟的能够通过的请求数,比如限流 qps 为100,算法的实现思路就是从第一个请求进来开始计时,在接下去的1s内,每来一个请求,就把计数加1,如果累加的数字达到了100,那么后续的请求就会被全部拒绝。等到1s结束后,把计数恢复成0,重新开始计数。
这种实现方式,相信大家都知道有一个弊端:如果我在单位时间1s内的前10ms,已经通过了100个请求,那后面的990ms,只能眼巴巴的把请求拒绝,我们把这种现象称为“突刺现象”
漏桶
为了消除”突刺现象”,可以采用漏桶算法实现限流,漏桶算法这个名字就很形象,算法内部有一个容器,类似生活用到的漏斗,当请求进来时,相当于水倒入漏斗,然后从下端小口慢慢匀速的流出。不管上面流量多大,下面流出的速度始终保持不变。
不管服务调用方多么不稳定,通过漏桶算法进行限流,每10毫秒处理一次请求。因为处理的速度是固定的,请求进来的速度是未知的,可能突然进来很多请求,没来得及处理的请求就先放在桶里,既然是个桶,肯定是有容量上限,如果桶满了,那么新进来的请求就丢弃。
这种算法,在使用过后也存在弊端:无法应对短时间的突发流量。特别像消息列队!
令牌桶
某种意义上讲,令牌桶算法是对漏桶算法的一种改进,桶算法能够限制请求调用的速率,而令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用。
在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择选择等待可用的令牌、或者直接拒绝。
放令牌这个动作是持续不断的进行,如果桶中令牌数达到上限,就丢弃令牌,所以就存在这种情况,桶中一直有大量的可用令牌,这时进来的请求就可以直接拿到令牌执行,比如设置 qps 为100,那么限流器初始化完成一秒后,桶中就已经有100个令牌了,这时服务还没完全启动好,等启动完成对外提供服务时,该限流器可以抵挡瞬时的100个请求。所以,只有桶中没有令牌时,请求才会进行等待,最后相当于以一定的速率执行。
PHP结合redis的限流代码
纯php写法
function Limiter($key = "qidong", $rate = 10, $max = 20, $default = 10): int {
$redis = new Redis();
$redis->connect("127.0.0.1", 6379);
$name = 'limiter';
// PHP 代码开始
// 从 KEYS 和 ARGV 数组中获取对应的值
$sKey = sprintf('%s:%s:store', $name, $key); // 即 Redis 键名、代表存储桶里的key
$nKey = sprintf('%s:%s:next', $name, $key); // 即 Redis 键名、代表下次请求key、存放时间戳
$now = time();
$rate = (int) $rate; //即每秒产生的令牌数量
$max = (int) $max; // 令牌桶的最大容量
$default = (int) $default; // 令牌桶的默认容量
// 从 Redis 中获取 sNum 的值
$sNum = (int) $redis->get($sKey);
// 判断 sNum 是否为空或 nil,如果是,则将 sNum 初始化为 0
if ($sNum === null) {
$sNum = 0;
}
// 从 Redis 中获取 nNum 的值
$nNum = (int) $redis->get($nKey);
// 判断 nNum 是否为空或 nil,如果是,则将 nNum 初始化为当前时间戳 $now,将 sNum 初始化为默认容量 $default
if ($nNum === null) {
$nNum = $now;
$sNum = $default;
}
// 计算新的令牌数量 newPermits
$newPermits = 0;
if ($now > $nNum) {
$newPermits = ($now - $nNum) * $rate + $sNum;
// 代表当前时间和上次生成令牌的时间的差乘以每秒可以生成rate加上剩余的令牌数量
// 将 sNum 更新为 newPermits个令牌和最大容量 max个令牌 中的较小值
$sNum = min($newPermits, $max);
}
// 初始化 isPermited 为 0
$isPermited = 0;
// 判断令牌数量 sNum 是否大于 0,如果是,则表示有令牌可用
if ($sNum > 0) {
// 将 sNum 减 1,表示消耗一个令牌
$sNum--;
// 将 isPermited 设为 1,表示允许访问
$isPermited = 1;
}
// 将更新后的令牌数量 sNum 和当前时间戳 now 存回 Redis
$redis->set($sKey, $sNum);
$redis->set($nKey, $now);
// 返回 isPermited,表示是否允许访问
return $isPermited;
// PHP 代码结束
}
<?php
// 其他 PHP 代码...
// 定义 Lua 脚本对应的 PHP 代码
$lua = <<<LUA
local sKey = KEYS[1];
local nKey = KEYS[2];
local now = tonumber(ARGV[1]);
local rate = tonumber(ARGV[2]);
local max = tonumber(ARGV[3]);
local default = tonumber(ARGV[4]);
local sNum = redis.call('get', sKey);
if((not sNum) or sNum == nil)
then
sNum = 0
end
sNum = tonumber(sNum);
local nNum = redis.call('get', nKey);
if((not nNum) or nNum == nil)
then
nNum = now
sNum = default
end
nNum = tonumber(nNum);
local newPermits = 0;
if(now > nNum)
then
newPermits = (now-nNum)*rate+sNum;
sNum = math.min(newPermits, max)
end
local isPermited = 0;
if(sNum > 0)
then
sNum = sNum -1;
isPermited = 1;
end
redis.call('set', sKey, sNum);
redis.call('set', nKey, now);
return isPermited;
LUA;
// 定义脚本参数
$sKey = 'your_skey';
$nKey = 'your_nkey';
$now = time();
$rate = 1; // 令牌产生速率
$max = 10; // 令牌桶最大容量
$default = 10; // 令牌桶默认容量
// 执行 Redis Lua 脚本
$args = [
$sKey,
$nKey,
$now,
$rate,
$max,
$default,
];
$redis = new Redis();
$redis->connect("127.0.0.1", 6379);
$result=$redis->eval($lua, $args, 2);
return (bool)$result;