file
无论你的项目是什么(RESTful API、Web 平台等),都可以利用 Laravel 提供的非常有用且直观的缓存机制去响应你的内容。 一般来说,不管你发送什么数据(HTML、JSON、collections、Eloquent 的对象等),Laravel 都会在其过期之前帮你存到缓存系统中。

下面是一个简单的使用缓存的代码段:

return Cache::remember($rememberKey, $minutes , function() use ($data) {
    return $data;
});

很容易理解,这上面调用了 remember() ,并且发送 $rememberKey (标识缓存的名字)以及到期时间(以分钟为单位) 来获取缓存中的数据。而一旦要找的缓存不存在,闭包函数里面的内容就被运行,并将返回的结果以 $rememberKey 为名、$minutes 为到期时间存储在缓存中。你可能会好奇这简短的一行居然实现了这么多步骤,深感 Laravel 之强大之优雅的同时,我们还是赶紧来刨根问底吧。

Laravel 的缓存机制中还有 put() 、 pull() 和 get() 等方法,具体看 文档 就能行了。可是这里一定要隆重地强调一下 remember() 这个方法,这货太好用了,包揽了查询和存储两大我们所需要的基本功能。

那么问题来了:Laravel 要怎么确定何时存储数据?

很简单,这个问题的关键是 $rememberKey 这个值,根据这个值 Laravel 会知道这个数据是否已经被缓存。因为没理由你会干出用同一个 $rememberKey 去作为所有你想缓存的数据的名字吧?(有我也是服了你了)

不过这对于其他场景来说并不是很管用。假设当你知道 $rememberKey 作为唯一值的重要性,你决定使用当前的 URL 作为缓存数据的标识,然后你就写了这样的代码:

$url = request()->url();
return Cache::remember($url, $minutes, function () use ($data) {
    return $data;
});

同样的道理,但现在上面的 $rememberKey 是当前请求的 URL,然后你就很得意地确定 $rememberKey 会根据请求而改变(即它不会是一个唯一值)。

例如,如果你收到对 yourdomain.com/products 的请求,然后使用该确切值去查找缓存来响应。 当用户跳转到另一个 URL(如 yourdomain.com/categories)时,同理只要确保返回缓存中相应的值并且相应地进行存储。

很方便对不?你也是这么做的么?那我该提醒你小心被自己写的坑反咬一口了~

假设你要对查询的结果进行排序或分页,那 URL 就会是这种情况:yourdomain.com/products?page=2&sort_by=price 。当你用 $url = request()->url() 获取当前的 URL 时,它不会获取查询参数(page=2&sort_by=price),这意味着查询参数引入的数据更改不会被返回出来。

觉得掉坑了?没关系!爬出来就是了!现在问题的关键是我们需要把包含查询参数拿出来就行了,那就换个方法:

$fullUrl = request()->fullUrl();
return Cache::remember($fullUrl, $minutes, function () use ($data) {
    return $data;
});

行了,这样就写就 OK 了!不相信?无所谓~有耐心的话,大可测试一下。

访问 yourdomain.com/products?page=2&sort_by=price ,它缓存数据并返回。再访问 yourdomain.com/products?page=1&sort_by=name ,它还是会再次缓存数据并返回。是不是很棒!它缓存不同的响应数据并返回。

可是,现在,我们还得最后再做一次测试。现在先访问 yourdomain.com/products?page=2&sort_by=price ,然后访问 yourdomain.com/products?sort_by=price&page=2。这是两个不同的 rememberKey,所以系统会分开缓存。但是啊!这两个请求返回的是同样的数据呀!按照正常人类思维逻辑,查询字符串的顺序并不重要,如果缓存数据里面存储了相同的数据,它应该返回同一个缓存数据,而不是两个 rememberKey 不同内容却相同的数据。

某种意义上来讲,问题被解决了,但是用的方式并不完美。也就是说,解决问题的方法,并不像我们开始时那么简单了。我们需要一种确定方式,独立于查询字符串的顺序,只要它们相同,就不要再次缓存数据。因此我们需要对查询的字符串进行排序。

现在我们开始为这个想法写代码:

$url = request()->url();
$queryParams = request()->query();

//对查询参数按照键名排序
ksort($queryParams);

//将查询数组转换为查询字符串
$queryString = http_build_query($queryParams);

$fullUrl = "{$url}?{$queryString}";

return Cache::remember($fullUrl, $minutes, function () use ($data) {
    return $data;
});

思路是这样的,我们只需使用 query() 获取 URL 上的查询参数的关联数组,然后使用 ksort 对key(而不是值)进行排序。 有 ksort 函数对数组按照键名进行排序,所以我们不需要再为排序的事情头疼。 然后把排序后的数组用 http_build_query() 构建查询用字符串,再重新与 URL 组合作为 rememberKey 的值。

这样一来,如果用户访问 yourdomain.com/products?sort_by=price&page=2,它就会与 yourdomain.com/products?page=2&sort_by=price 相同。即使 URL 根据请求看起来有点不同,它们也会共用同一个缓存数据。

你还可以稍微『花哨』些,比如像这样把 $fullUrl 用 sha1 进行加密:

$url = request()->url();
$queryParams = request()->query();

ksort($queryParams);

$queryString = http_build_query($queryParams);

$fullUrl = "{$url}?{$queryString}";

$rememberKey = sha1($fullUrl);

return Cache::remember($rememberKey, $minutes, function () use ($data) {
    return $data;
});

这里是下班了还在不停翻译的小骏蜂~
写代码这件事情,可以是一个人加上一群人的狂欢,毕竟很酷!

更多资讯教程可以上 Laravel 资讯站 查看~

本文翻译改编自 JuanDMeGon 的 Utilizing Laravel’s Cache with Query Params

回复数量: 20
  •    
  • 839891627
    839891627 ⋅ 
     ⋅ 1年前

    看了左上角,发现性别是女 原来 laravel 还有媛 点赞 

  • ab0029
    ab0029 ⋅ 
     ⋅ 1年前

    手动赞

  • zhangbao O ever youthful, O ever weeping! ⋅ 
     ⋅ 10个月前

    我有个问题:用 sha1 算法加密的话,有必要吗?

    如果序列化参数加上 URL 就OK了,就没必要再加密了,对吧。

  • AnnEason
    AnnEason ⋅ 
     ⋅ 10个月前

    真棒!!

  • godruoyi 二楞徐的闲谈杂鱼 ⋅ 
     ⋅ 10个月前

    直接使用Illuminate\Http\Requestfingerprint方法不是更好吗?

    public function fingerprint()
    {
        if (! $route = $this->route()) {
            throw new RuntimeException('Unable to generate fingerprint. Route unavailable.');
        }
        return sha1(implode('|', array_merge(
            $route->methods(), [$route->domain(), $route->uri(), $this->ip()]
        )));
    }
  • JokerLinly 安正超粉丝协会会长大人  ⋅ 
     ⋅ 10个月前

    @zhangbao 一个缓存你加密它干嘛。。

  • JokerLinly 安正超粉丝协会会长大人  ⋅ 
     ⋅ 10个月前

    @zhangbao 我是觉得花哨了

  • zhangbao O ever youthful, O ever weeping! ⋅ 
     ⋅ 10个月前

    @JokerLinly 我是觉得多余了,没必要。序列化的 fullUrl 完全能满足需要了。

  • tradzero
    tradzero ⋅ 
     ⋅ 10个月前

    不知道是不是我理解错了 fullUrl源码里调用的symfony的normalizeQueryString 已经对queryString 排序过了呀
    yourdomain.com/products?sort_by=price&page=2yourdomain.com/products?sort_by=price&page=2打印出来的结果是一样的呀

  • JokerLinly 安正超粉丝协会会长大人  ⋅ 
     ⋅ 10个月前

    @tradzero 对你的问题表示一脸懵逼

  • wujunze 
  • springjk 西北一匹狼 3 ⋅ 
     ⋅ 9个月前

    @zhangbao 我个人认为加密是有必要的,因为这个 $rememberKey 是要作为 Redis、数据库等的 Search key 的,所以理论上越短越好,遇到搜索条件多的 URL 是很长的,这时摘要一下能有效的减小 key 的长度,在数据量大的情况下可以提升一定的性能,我个人更偏向于用 16 位的 MD5,MD5 作为签名算法使用至少还有长度更短这一点优势。

    另外,如果你的业务需要能经常去检查缓存池的话,在 console 里这个 key 就不直观了,所以这里也要看实际需求,但总的来说,我认为大部分情况下进行摘要是更好一些的做法(至少 console 里能排的整齐一些不是吗:smile:)。

  • zhangbao O ever youthful, O ever weeping! ⋅ 
     ⋅ 9个月前

    @springjk 确实,我没有考虑到 URL 过长的情况,感谢你的回答,涨知识了!@JokerLinly

  • JokerLinly 安正超粉丝协会会长大人  ⋅ 
     ⋅ 9个月前

    @zhangbao 嗯,我也没考虑过呢

  • JokerLinly 安正超粉丝协会会长大人  ⋅ 
     ⋅ 9个月前

    @zhangbao 毕竟太长我宁愿用 POST

  • zhangbao O ever youthful, O ever weeping! ⋅ 
     ⋅ 9个月前

    @JokerLinly URL 太长跟用不用 POST 有关系吗?

  •