laravel 默认不自动标准化get参数,/?a=1&b=2和/?b=2&a=1被视为不同缓存键,导致缓存击穿;需手动解析、过滤空值、按键排序、重建查询串生成唯一键。

直接说结论:Laravel 默认不自动去重 GET 请求参数顺序或空值,/?a=1&b=2 和 /?b=2&a=1 会被视为两个不同缓存键,导致缓存击穿、重复请求、TTL 浪费。必须手动标准化 URL 查询参数。
为什么 Cache::remember 或 ResponseCache 会缓存重复参数变体
Laravel 的缓存键生成逻辑(包括 Cache::remember 和 ResponseCache)默认只对原始请求 URI 做哈希或直接拼接,不解析、不排序、不过滤空参数。比如:
/api/search?keyword=laravel&page=1&sort=desc/api/search?page=1&keyword=laravel&sort=desc/api/search?keyword=laravel&page=1&sort=&limit=
这三条请求在底层都会生成不同缓存键,哪怕业务上完全等价。尤其当前端 SDK 或代理层自动重排参数时,问题更隐蔽。
如何标准化 GET 参数生成唯一缓存键
核心是「解析 → 过滤 → 排序 → 序列化」四步。不要依赖 http_build_query($_GET),它不排序也不过滤空值。
- 用
parse_str($request->server('QUERY_STRING'), $params)解析原始查询字符串,保留原始类型(如数字不转字符串) - 用
array_filter($params, fn($v) => $v !== '' && $v !== null)清除空值和 null(注意:若业务需保留foo=null,则改用显式白名单) - 用
ksort($params)强制按键字母升序排列,确保a=1&b=2和b=2&a=1输出一致 - 最后用
http_build_query($params)生成标准查询串,再拼入完整 URI 或直接哈希:sha1($baseUri . '?' . $normalizedQuery)
示例片段(用于自定义中间件或封装客户端):
$query = $request->server('QUERY_STRING') ?: '';
parse_str($query, $params);
$params = array_filter($params, fn($v) => $v !== '' && $v !== null);
ksort($params);
$normalized = http_build_query($params);
$cacheKey = 'api_' . sha1($request->fullUrlWithQuery([]) . '?' . $normalized);
ResponseCache 包怎么避免参数乱序缓存
ResponseCache 默认使用 Request::fullUrl() 作为键源,它返回未标准化的原始 URL。必须覆盖其 CacheKeyGenerator 实现。
- 新建类
App\Http\CacheKeys\NormalizedGetCacheKey,继承OptimistDigital\ResponseCache\CacheKeys\CacheKeyGenerator - 重写
generateKey(Request $request)方法,套用上面的标准化逻辑 - 在
config/responsecache.php中把'cache_key_generator' => App\Http\CacheKeys\NormalizedGetCacheKey::class
注意:不能只替换 fullUrlWithQuery([]) —— 它仍保留参数顺序;必须显式解析+排序。
Cache::remember 场景下如何复用标准化键
别在闭包里临时拼键。把标准化逻辑抽成可复用方法,比如放在 app/Support/UrlNormalizer.php:
public static function toCacheKey(string $url): string
{
$parts = parse_url($url);
parse_str($parts['query'] ?? '', $params);
$params = array_filter($params, fn($v) => $v !== '' && $v !== null);
ksort($params);
$normalized = http_build_query($params);
return sha1($parts['path'] . '?' . $normalized);
}
然后在控制器中这样用:
$key = UrlNormalizer::toCacheKey($request->fullUrl());
$data = Cache::remember($key, 300, function () use ($request) {
return Http::get($request->fullUrl())->json();
});
复杂点在于:有些 API 对空参数语义敏感(比如 ?include= 表示包含全部字段),这种要单独加白名单过滤逻辑,不能一刀切 array_filter。最容易被忽略的是参数编码差异 —— %20 和 + 都表示空格,但 parse_str 会统一处理,这点反而安全。真正要盯住的是业务语义是否允许丢弃空参。











