必须用redis+lua脚本实现原子性限流,否则并发下计数器失准;需编写lua脚本、构造唯一键、执行eval并判断返回值,不可用cache()->increment()替代;laravel内置ratelimiter已支持lua限流,但老版本需确认redis配置正确。

在Laravel项目中应对每秒数千次请求的接口,必须用Redis+Lua脚本实现原子性限流,否则多进程并发下计数器会严重失准,导致超限放行或误拦截。
准备Lua限流脚本
在resources/scripts/rate_limit.lua中写入固定窗口计数逻辑:
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local current = redis.call('GET', key)
if current == false then
redis.call('INCR', key)
redis.call('EXPIRE', key, window)
return 1
elseif tonumber(current) return redis.call('INCR', key)
else
return -1
end
这个脚本只做三件事:查当前值→未超限则自增→设置过期时间。所有操作在Redis单线程内一次完成,【避免了客户端先GET再INCR再EXPIRE造成的竞态漏洞】。
注册脚本并绑定到Redis连接
在App\Providers\AppServiceProvider.php的boot()方法中加载脚本:
$luaScript = file_get_contents(resource_path('scripts/rate_limit.lua'));
$this->app['redis']->connection()->eval($luaScript, 1, 'rate:limit:'.$request->ip());
注意:不要用evalsha首次调用,必须确保脚本已加载;生产环境可改用EVALSHA提升性能,但需提前用SCRIPT LOAD预存。
在控制器中调用限流逻辑
第一步:构造唯一限流键,例如按IP+路由组合:$key = 'rate:ip:'.$request->ip().':route:'.Route::currentRouteName();
第二步:执行Lua脚本,传入窗口大小与阈值:$result = Redis::eval($luaScript, 1, $key, 100, 60);
第三步:判断返回值:
返回-1 → 拒绝请求,返回429状态码;
返回正整数 → 允许通行,可将剩余次数写入响应头X-RateLimit-Remaining。
这一步不能用cache()->increment()替代——Laravel缓存驱动在Redis底层仍会拆成多个命令,无法保证原子性。
适配Laravel RateLimiter门面(可选)
方法一:直接复用Laravel内置限流器
在App\Providers\RouteServiceProvider.php中配置:
RateLimiter::for('api', function (Request $request) {<br> return Limit::perMinute(100)->by($request->ip());<br>});
该方式底层已自动使用Lua脚本,无需额外编码,但仅支持固定窗口算法。
方法二:自定义限流器类
创建App\RateLimiting\TokenBucketLimiter.php,在attempt()中调用前述Lua脚本,传入令牌桶参数(容量、速率、时间戳),返回布尔值。
注意:Laravel 11+默认启用Redis Lua限流,但老版本需确认config/cache.php中default为redis,且redis配置项下options包含'prefix' => 'laravel_cache:',否则键名冲突会导致限流失效。











