由于系统调用的下游服务存在限流,为了保障服务,在调用下游服务前添加一个限流策略,php基于滑动窗口,redis,lua脚本实现
/**
*
* @param mixed $redis
* @param mixed $rateLimitKey key
* @param mixed $timeWindow 时间窗口,单位毫秒
* @param mixed $limit 窗口内允许通过数
* @throws \Concrete\Exceptions\BusinessException
* @return bool
*/
protected function rateLimit($rateLimitKey, $timeWindow = 10000, $limit = 50) // timeWindow 以毫秒为单位
{
$luaScript = <<<'LUA'
local key = KEYS[1]
local timeWindow = tonumber(ARGV[1])
local limit = tonumber(ARGV[2])
-- 获取当前时间(毫秒)
local currentTime = redis.call('TIME')
local now = tonumber(currentTime[1]) * 1000 + tonumber(currentTime[2]) / 1000
local windowStart = now - timeWindow
redis.replicate_commands()
-- 移除过期的请求(滑动窗口:移除超过 timeWindow 毫秒的请求)
redis.call('ZREMRANGEBYSCORE', key, '-inf', windowStart)
-- 获取当前窗口内请求的数量
local requestsInWindow = redis.call('ZCARD', key)
if requestsInWindow >= limit then
-- 请求超过限制,返回 表示限流
return 2
end
-- 添加当前请求
redis.call('ZADD', key, now, now)
-- 若 key 无过期时间则设置过期时间
local ttl = redis.call('PTTL', key)
if ttl < 0 then
redis.call('PEXPIRE', key, timeWindow)
end
-- 返回 1 表示请求通过
return 1
LUA;
if (!$this->redis) {
Log::error("Redis connection error: redis不可用");
return true;
}
try {
$result = $this->redis->eval($luaScript, 1, $rateLimitKey, $timeWindow, $limit);
} catch (\Exception $e) {
Log::error("redis eval抛异常: " . $e->getMessage() . ",key: " . $rateLimitKey);
return true;
}
$error = $this->redis->getLastError();
if ($error) {
Log::error("redis eval执行有错: " . $error . ",key: " . $rateLimitKey);
return true;
}
switch($result){
case 1:
return true;
case 2:
throw new HttpException(Code::SERVICE_CURRENT_LINT, "Too Many Requests");
default:
//脚本执行异常,打印异常消息,放行
Log::error("redis eval结果非预期,结果为:" . $result . ",key: " . $rateLimitKey);
}
return true;
}


被折叠的 条评论
为什么被折叠?



