在Laravel项目里,使用缓存计数器实现限流是非常实用的方案,核心思路是通过缓存记录某个标识在单位时间内的请求次数,超过阈值就拒绝请求。常用的缓存驱动可以选择Redis,因为它的原子操作特性可以避免并发场景下的计数错误。

核心实现逻辑
限流的核心流程分为三步:首先获取当前请求的标识,比如用户ID或者客户端IP;然后尝试从缓存中读取该标识对应的请求计数,如果不存在就初始化为1并设置过期时间;最后判断计数是否超过设定的阈值,超过则返回限流响应,未超过则递增计数。
基础限流方法实现
我们可以先封装一个通用的限流方法,方便在不同接口中调用,以下是基于Redis缓存的实现代码:
<?php
namespace AppServices;
use IlluminateSupportFacadesCache;
use IlluminateSupportFacadesRedis;
class RateLimitService
{
/**
* 基于缓存计数器实现限流
* @param string $key 限流标识,比如用户ID、IP等
* @param int $maxAttempts 单位时间最大请求次数
* @param int $decayMinutes 时间窗口,单位分钟
* @return bool 是否允许请求,true允许,false拒绝
*/
public function attempt(string $key, int $maxAttempts, int $decayMinutes): bool
{
// 缓存键名,加上前缀避免冲突
$cacheKey = 'rate_limit:' . $key;
// 使用Redis的INCR原子操作递增计数
$count = Redis::incr($cacheKey);
// 如果是第一次请求,设置过期时间
if ($count === 1) {
Redis::expire($cacheKey, $decayMinutes * 60);
}
// 判断计数是否超过阈值
return $count <= $maxAttempts;
}
/**
* 获取当前剩余可请求次数
* @param string $key 限流标识
* @param int $maxAttempts 最大请求次数
* @return int 剩余次数
*/
public function remaining(string $key, int $maxAttempts): int
{
$cacheKey = 'rate_limit:' . $key;
$count = Redis::get($cacheKey) ?? 0;
return max(0, $maxAttempts - $count);
}
}
在控制器中使用限流
封装好服务之后,就可以在控制器的方法中调用限流逻辑,以下是用户登录接口的限流示例,限制同一个IP每分钟最多请求5次:
<?php
namespace AppHttpControllers;
use AppServicesRateLimitService;
use IlluminateHttpRequest;
use IlluminateSupportFacadesResponse;
class AuthController extends Controller
{
protected $rateLimitService;
public function __construct(RateLimitService $rateLimitService)
{
$this->rateLimitService = $rateLimitService;
}
public function login(Request $request)
{
// 获取客户端IP作为限流标识
$ip = $request->ip();
// 限制每分钟最多5次请求
$maxAttempts = 5;
$decayMinutes = 1;
if (!$this->rateLimitService->attempt($ip, $maxAttempts, $decayMinutes)) {
// 限流触发,返回错误响应
$remaining = $this->rateLimitService->remaining($ip, $maxAttempts);
return Response::json([
'code' => 429,
'msg' => '请求过于频繁,请稍后再试',
'remaining' => $remaining
], 429);
}
// 正常登录逻辑
// ...
return Response::json(['code' => 200, 'msg' => '登录成功']);
}
}
注意事项
- 缓存驱动选择:如果并发量较高,建议使用Redis作为缓存驱动,避免文件缓存或者数据库缓存的原子性问题导致计数错误。
- 标识唯一性:限流标识需要根据场景选择,用户相关接口可以用用户ID,公开接口可以用IP,也可以组合多个维度比如IP加接口路径。
- 过期时间设置:时间窗口的过期时间要和计数器的过期时间一致,避免计数器过期后还残留旧数据影响限流效果。
- 分布式场景:如果是多台服务器部署的Laravel项目,必须保证所有服务器使用同一个缓存服务,否则限流会失效。
扩展优化
如果需要更灵活的限流规则,比如滑动时间窗口限流,可以在上述基础上做扩展,每次请求时先清理过期的计数记录,再统计当前窗口内的请求次数。另外也可以结合Laravel的中间件,把限流逻辑封装成中间件,在路由中直接调用,减少重复代码。