Laravel 缓存花样耍

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 上的查询参数的关联数组,然后使用 ksortkey(而不是值)进行排序。 有 ksort 函数对数组按照键名进行排序,所以我们不需要再为排序的事情头疼。 然后把排序后的数组用 http_build_query() 构建查询用字符串,再重新与 URL 组合作为 rememberKey 的值。

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

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

$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 资讯站 查看~

本文翻译改编自 JuanDMeGonUtilizing Laravel’s Cache with Query Params

本作品采用《CC 协议》,转载必须注明作者和本文链接
Stay Hungry, Stay Foolish.
本帖由 Summer 于 7年前 加精
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 22

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

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

7年前 评论

:+1: 小骏蜂

7年前 评论

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

7年前 评论

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

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

7年前 评论
godruoyi

直接使用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()]
    )));
}
7年前 评论

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

7年前 评论

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

7年前 评论

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

7年前 评论

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

7年前 评论

手动点赞?

7年前 评论
王老板的前端

来点赞!必须点赞.

7年前 评论

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

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

7年前 评论

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

7年前 评论

@zhangbao 嗯,我也没考虑过呢:smile:

7年前 评论

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

7年前 评论

@springjk 哈希碰撞了解一下 :blush:

6年前 评论

大神互怼,长知识...哈

6年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!