Laravel 中的缓存介绍

Posted by

缓存是使 Web 应用程序快速运行的重要工具。这很容易实现,但很难做到正确。如何使用 Laravel 的内置系统实现缓存,该系统支持 Memcached 和 Redis 等多种后端。

缓存是一种在“缓存”或高速存储层中存储数据的方法。它通常(但不总是)存储在内存中;因此,从它而不是从数据库或文件存储中存储和获取数据更快。

例如,假设我们有一些数据库查询需要执行并且需要很长时间或使用大量内存才能运行。为了减少这种开销,我们可以执行查询,然后将其结果存储在缓存中。这意味着下次我们需要运行这个查询时,我们可以从缓存中获取结果,而不是再次执行数据库查询。

同样,我们也可以使用这种方法来缓存外部 API 请求的响应。例如,假设您需要向汇率 API 发出请求,以查找给定日期和时间两种货币之间的汇率。我们可以在获取一次后缓存这个结果,这样如果我们再次需要这个汇率,我们就不需要再发出任何请求了。

Laravel提供了一个简单而强大的 API 来与缓存交互。默认情况下,Laravel 支持使用 RedisMemcachedDynamoDB、数据库、文件和数组进行缓存。

使用缓存的好处

如上所述,如果使用得当,缓存可以减少文件存储系统、数据库和应用程序服务器的工作量。例如,假设您有一个 CPU 和内存密集型的复杂数据库查询。如果此查询不经常运行,则问题可能不会太大。但是,如果定期运行此查询,您可能会开始注意到应用程序的性能受到影响。资源密集型查询会减慢给定查询的速度,并可能影响系统的其他部分以供其他用户使用。

因此,为了减少此查询引起的基础设施开销,我们可以执行它,然后将其结果存储在缓存中。通过这样做,下次我们需要获取这些结果时,我们将能够从缓存中检索它们,而不必再次运行数据库查询。

如果我们需要运行昂贵的 PHP 脚本或定期读取文件,我们也可以应用相同的逻辑。通过缓存脚本的输出或文件的内容,我们可以减少应用服务器的开销。

同样,如上所述,我们可以缓存来自外部 API 请求的响应,这样我们就不需要进行重复查询。通过减少查询,我们还可以减少达到 API 可能施加的节流限制或每月请求限制的机会。

众所周知,加载时间每天都变得越来越重要,尤其是现在谷歌正在使用加载时间作为排名因素。研究表明,用户往往会离开加载时间超过 3 秒的页面。假设我们有一个需要几秒钟才能执行的数据库查询。现在,一开始几秒钟似乎并不算太​​糟糕。但是如果你将这几秒钟与你的 Laravel 应用程序的其他部分加载页面所花费的时间结合起来,你可以看到它是如何开始超过三秒阈值的。

因此,由于从缓存中检索数据的速度有多快,我们可以存储查询的结果,这样我们就不需要再次执行耗时的查询。

在缓存中存储数据

现在我们已经介绍了缓存是什么的基础知识以及使用它的好处,让我们探索如何在 Laravel 应用程序中使用它。出于本文的目的,我们将使用极其简单的查询,但它们仍应解释 Laravel 中缓存的整体概念。

假设我们有一个方法可以查询数据库以获取所有符合给定条件的用户,如下例所示:

public function getUsers($foo, $bar)
{
    return User::query()
        ->where('foo', $foo)
        ->where('bar', $bar)
        ->get();
}

如果我们想更新此方法以缓存此查询的结果,我们可以使用该forever()方法,该方法将获取用户查询的结果,然后将结果存储在缓存中而没有到期日期。下面的例子展示了我们如何做到这一点:

public function getUsers($foo, $bar)
{
    $users = User::query()
        ->where('foo', $foo)
        ->where('bar', $bar)
        ->get();

    Cache::forever("user_query_results_foo_{$foo}_bar_{$bar}", $users);

    return $user;
}

缓存通常是键值对存储,这意味着它具有对应于值(实际缓存数据)的唯一键。因此,无论何时尝试在缓存中存储或检索任何内容时,都需要确保使用唯一键,以免覆盖任何其他缓存数据。

正如您在上面的示例中所看到的,我们已将$foo$bar参数附加到缓存键的末尾。这样做允许我们为每个可能被使用的参数创建一个唯一的缓存键。

有时您可能希望将数据存储在数据库中但有一个到期日期。如果您正在缓存用于仪表板或报表上的统计信息的昂贵查询的结果,这将很有用。

因此,如果我们想将上面的用户查询缓存 10 分钟,我们可以使用以下put()方法:

public function getUsers($foo, $bar)
{
    $users = User::query()
        ->where('foo', $foo)
        ->where('bar', $bar)
        ->get();

    Cache::put("user_query_results_foo_{$foo}_bar_{$bar}", $users, now()->addMinutes(10));

    return $users;

现在,在 10 分钟过去后,该项目将过期并且将无法从缓存中检索(除非它已被重新添加)。

从缓存中获取数据

假设您要更新上述示例中的用户查询,以检查结果是否已被缓存。如果有,我们可以从缓存中获取并返回它。否则,我们可以执行数据库查询,将结果缓存 10 分钟,然后返回。为此,我们可以使用以下代码:

public function getUsers($foo, $bar)
{
    $cacheKey = "user_query_results_foo_{$foo}_bar_{$bar}";

    if (Cache::has($cacheKey)) {
        return Cache::get($cacheKey);
    }

    $users = User::query()
        ->where('foo', $foo)
        ->where('bar', $bar)
        ->get();

    Cache::put($cacheKey, $users, now()->addMinutes(10));

    return $users;
}

尽管这看起来仍然可读,但我们可以使用 Laravel 的remember()方法来进一步简化代码并降低其复杂性。此方法的工作方式与我们上面的示例相同:

public function getUsers($foo, $bar)
{
    return Cache::remember("user_query_results_foo_{$foo}_bar_{$bar}", now()->addMinutes(10), function () use ($foo, $bar) {
        return User::query()
            ->where('foo', $foo)
            ->where('bar', $bar)
            ->get();
    });
}

在请求的整个生命周期中缓存数据

有时您可能需要在同一个请求中多次运行同一个查询。作为一个非常基本的示例,假设您想要缓存来自我们上面示例的用户查询,但仅在请求期间。这样,您仍然可以获得速度优势并减少缓存的资源使用,但不必担心数据会在其他请求中持续存在。

为此,我们可以使用arrayLaravel 提供的缓存驱动程序。该驱动程序将数据存储在缓存中的 PHP 数组中,这意味着一旦请求完成运行,缓存的数据将被删除。

为了展示我们如何在请求生命周期内缓存上面的用户查询,我们可以使用以下内容:

public function getUsers($foo, $bar)
{
    return Cache::store('array')->remember("user_query_results_foo_{$foo}_bar_{$bar}", function () use ($foo, $bar) {
        return User::query()
            ->where('foo', $foo)
            ->where('bar', $bar)
            ->get();
    });
}

正如您在示例中所看到的,我们曾经store('array')选择array缓存驱动程序。

使用 Laravel 的缓存命令

由于 Laravel 的运行方式,它会启动框架并解析每个请求的路由文件。这需要读取文件,解析其内容,然后以您的应用程序可以使用和理解的方式保存它。因此,Laravel 提供了一个命令来创建一个可以更快地解析的路由文件:

php artisan route:cache

但请注意,如果您使用此命令并更改路线,则需要确保运行以下命令:

php artisan route:clear

这将删除缓存的路由文件,以便您可以注册较新的路由。

与路由缓存类似,每次发出请求时,Laravel 都会启动,并读取和解析项目中的每个配置文件。因此,为了防止需要处理每个文件,您可以运行以下命令,该命令将创建一个缓存配置文件:

php artisan config:cache

不过,就像上面的路由缓存一样,您需要记住每次更新 .env 文件或配置文件时运行以下命令:

php artisan config:clear

同样,Laravel 还提供了另外两个命令,可用于缓存视图和事件,以便在向 Laravel 应用程序发出请求时预编译并准备好它们。要缓存事件和视图,可以使用以下命令:

php artisan event:cache
php artisan view:cache

但是,与所有其他缓存命令一样,只要通过运行以下命令对代码进行任何更改,您都需要记住破坏这些缓存:

php artisan event:clear
php artisan view:clear

缓存的常见陷阱

尽管缓存可以在性能方面带来巨大的好处,但如果实施不正确,也会导致很多问题。

例如,如果缓存的数据没有在正确的时间被销毁,它可能会导致不正确的过时和陈旧数据。为了对此进行上下文化,让我们假设我们的 Laravel 应用程序在数据库中存储了一些设置,并且我们在每次页面加载时获取这些设置。因此,为了提高速度,我们决定将设置缓存 10 分钟。如果我们忘记在每次更新设置时清除代码中的缓存,可能会导致设置不正确长达 10 分钟,然后自动清除。

此外,如果在不需要的地方使用缓存,则很容易增加代码库的复杂性。如上例所示,缓存在 Laravel 中很容易实现。但是,可能会争辩说,如果通过缓存特定查询或方法没有获得太多性能提升,则可能不需要使用它。在错误的地方添加过多的缓存会导致难以追踪的错误和不可预测的行为。因此,最好将其添加到您获得明显改善的地方。

此外,如果您要向 Laravel 应用程序添加缓存,这通常意味着您需要在应用程序的基础架构中添加缓存服务器,例如 Redis。使用这种方法可能会引入额外的攻击向量,并在服务器上打开一个额外的端口,但如果您的应用程序的基础设施安全配置得当,这应该不是问题。但是,绝对应该记住这一点,尤其是在您缓存个人身份信息 (PII) 时,因为您不想冒任何缓存数据因攻击而泄露的风险。

结论

希望本文清楚地展示了缓存是什么以及如何在自己的 Laravel 应用程序中实现它以提高性能。正如你所看到的,由于 Laravel 提供的 API,缓存可以很容易地添加到你的应用程序中。但是,重要的是要记住添加时最常见的陷阱,例如记住破坏缓存以防止过时或过时的数据。