缓存
介绍
你的应用程序可能会执行一些数据检索或处理任务,这些任务可能会耗费大量的 CPU 或者需要几秒钟才能完成。在这种情况下,常常会对检索到的数据进行缓存,以便在后续对相同数据的请求时能够快速检索到。缓存的数据通常存储在诸如 Memcached 或 Redis 这样非常快速的数据存储中。
幸运的是,Laravel 提供了一个统一的、表达力强的 API,用于各种缓存后端,让您能够利用其快速数据检索和加速 Web 应用程序的速度。
配置
您的应用程序的缓存配置文件位于 config/cache.php
。在这个文件中,您可以指定要在整个应用程序中默认使用的缓存驱动程序。Laravel 支持流行的缓存后端,如 Memcached、Redis、DynamoDB 和关系型数据库。此外,还提供了基于文件的缓存驱动程序,而数组和"null"缓存驱动程序为自动化测试提供了便利的缓存后端。
缓存配置文件还包含其他各种选项,这些选项在文件中有记录,所以请确保阅读这些选项。默认情况下,Laravel 配置为使用文件缓存驱动程序,该驱动程序将序列化的缓存对象存储在服务器的文件系统中。对于较大的应用程序,建议您使用更强大的驱动程序,如 Memcached 或 Redis。您甚至可以为同一个驱动程序配置多个缓存配置。
驱动程序前提条件
数据库
使用数据库缓存驱动程序时,您需要设置一个表来包含缓存项。下面是一个表的示例架构声明:
Schema::create('cache', function (Blueprint $table) {
$table->string('key')->unique();
$table->text('value');
$table->integer('expiration');
});
您还可以使用
php artisan cache:table
Artisan 命令生成一个具有正确架构的迁移。
Memcached
使用 Memcached 驱动程序需要安装 Memcached PECL 扩展。您可以在 config/cache.php
配置文件中列出所有 Memcached 服务器。此文件已经包含了一个 memcached.servers
条目,以帮助您入门:
'memcached' => [
'servers' => [
[
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
'port' => env('MEMCACHED_PORT', 11211),
'weight' => 100,
],
],
],
如果需要,您可以将主机选项设置为 UNIX socket 路径。如果这样做,端口选项应设置为 0
:
'memcached' => [
[
'host' => '/var/run/memcached/memcached.sock',
'port' => 0,
'weight' => 100
],
],
Redis
在使用 Redis 缓存之前,您需要通过 PECL 安装 PhpRedis PHP 扩展,或者通过 Composer 安装 predis/predis
包(~1.0)。Laravel Sail 已经包含了这个扩展。此外,官方的 Laravel 部署平台,如 Laravel Forge 和 Laravel Vapor,默认安装了 PhpRedis 扩展。
有关配置 Redis 的更多信息,请参阅 Laravel 文档页面。
DynamoDB
在使用 DynamoDB 缓存驱动程序之前,您必须创建一个 DynamoDB 表来存储所有缓存数据。通常,这个表的名称应该是 cache
。但是,您应该根据应用程序的缓存配置文件中 stores.dynamodb.table
配置值的内容来命名表名。
这个表还应该有一个字符串分区键,其名称与应用程序的缓存配置文件 stores.dynamodb.attributes.key
配置项的值相对应。默认情况下,分区键应命名为 key
。
缓存用法
获取一个缓存实例
要获取一个缓存存储实例,可以使用 Cache
门面,这是我们在整个文档中使用的方法。Cache
门面提供了对 Laravel 缓存契约的底层实现的方便、简练的访问:
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Facades\Cache;
class UserController extends Controller
{
/**
* Show a list of all users of the application.
*/
public function index(): array
{
$value = Cache::get('key');
return [
// ...
];
}
}
访问多个缓存存储
使用 Cache
门面,您可以通过 store
方法访问各种缓存存储。传递给 store
方法的键应该与缓存配置文件中的存储配置数组中的一个存储相对应:
$value = Cache::store('file')->get('foo');
Cache::store('redis')->put('bar', 'baz', 600); // 10 Minutes
从缓存中检索项目
使用 Cache
门面的 get
方法从缓存中检索项目。如果项目不存在于缓存中,将返回 null
。如果您愿意,可以向 get
方法传递第二个参数,指定在项目不存在时希望返回的默认值:
$value = Cache::get('key');
$value = Cache::get('key', 'default');
您甚至可以将闭包作为默认值。如果缓存中指定的项目不存在,将返回闭包的结果。通过传递闭包,您可以推迟从数据库或其他外部服务检索默认值的操作:
$value = Cache::get('key', function () {
return DB::table(/* ... */)->get();
});
检查项目是否存在
可以使用 has
方法来判断缓存中是否存在一个项目。如果项目存在但其值为 null
,该方法也将返回 false
:
if (Cache::has('key')) {
// ...
}
增加/减少值
increment
和 decrement
方法可以用来调整缓存中整数项目的值。这两个方法都接受一个可选的第二个参数,表示要增加或减少项目值的数量:
// Initialize the value if it does not exist...
Cache::add('key', 0, now()->addHours(4));
// Increment or decrement the value...
Cache::increment('key');
Cache::increment('key', $amount);
Cache::decrement('key');
Cache::decrement('key', $amount);
检索和存储
有时您可能希望从缓存中检索一个项目,但如果请求的项目不存在,也存储一个默认值。例如,您可能希望从缓存中检索所有用户,或者如果它们不存在,则从数据库中检索它们并将它们添加到缓存中。您可以使用Cache::remember
方法实现这一点:
$value = Cache::remember('users', $seconds, function () {
return DB::table('users')->get();
});
如果项目不存在于缓存中,传递给 remember
方法的闭包将被执行,并且其结果将放置在缓存中。
您可以使用 rememberForever
方法从缓存中检索一个项目,或者永久存储它(即使它不存在):
$value = Cache::rememberForever('users', function () {
return DB::table('users')->get();
});
检索和删除
如果您需要从缓存中检索一个项目,然后删除该项目,可以使用 pull
方法。与 get
方法一样,如果项目不存在于缓存中,将返回 null
:
$value = Cache::pull('key');
将项目存储在缓存中
您可以使用 Cache
门面上的 put
方法将项目存储在缓存中:
Cache::put('key', 'value', $seconds = 10);
如果未将存储时间传递给 put
方法,项目将无限期存储:
Cache::put('key', 'value');
您可以传递一个 DateTime
实例来表示缓存项的期望到期时间,而不是将秒数作为整数传递给 put
方法:
Cache::put('key', 'value', now()->addMinutes(10));
如果不存在
如果缓存中不存在某个项,则 add
方法只会在缓存中添加该项。如果项目实际上已添加到缓存中,则该方法将返回 true
。否则,该方法将返回 false
。add
方法是一个原子操作:
Cache::add('key', 'value', $seconds);
永久存储项
forever
方法可以永久将项存储在缓存中。由于这些项不会过期,您必须使用 forget
方法手动从缓存中删除它们:
Cache::forever('key', 'value');
如果您使用的是 Memcached 驱动程序,并且“永久”存储的项达到了缓存的大小限制,则这些项可能会被删除。
从缓存中删除项目
可以使用 forget
方法从缓存中删除项目:
Cache::forget('key');
您还可以提供一个零或负的过期秒数来删除项目:
Cache::put('key', 'value', 0);
Cache::put('key', 'value', -5);
您可以使用 flush
方法清除整个缓存:
Cache::flush();
清除缓存时不会遵守配置的缓存“前缀”,并且将删除缓存中的所有条目。在清除由其他应用程序共享的缓存时,请小心考虑这一点。
缓存助手
除了使用 Cache
门面,您还可以使用全局的 cache
函数通过缓存来检索和存储数据。当 cache
函数以单个字符串参数调用时,它会返回给定键的值:
$value = cache('key');
如果将一个键/值对数组和一个过期时间提供给函数,它将在缓存中存储对应持续时间的值:
cache(['key' => 'value'], $seconds);
cache(['key' => 'value'], now()->addMinutes(10));
当 cache
函数调用时没有任何参数,它返回一个 Illuminate\Contracts\Cache\Factory
实现的实例,使您能够调用其他缓存方法:
cache()->remember('users', $seconds, function () {
return DB::table('users')->get();
});
在测试调用全局的 cache
函数时,您可以像测试门面一样使用 Cache::shouldReceive
方法。
缓存标签
当使用文件、dynamodb或数据库缓存驱动程序时,不支持缓存标签。此外,对于使用存储“永久”的多个标签的缓存,使用
memcached
等驱动程序会获得最佳性能,这些驱动程序会自动清除过期记录。
存储带有标签的缓存项
缓存标签允许您对缓存中的相关项进行标记,然后刷新分配了给定标签的所有缓存值。可以通过传入一个有序的标签名称数组来访问带标签的缓存。例如,让我们访问一个带标签的缓存并将一个值放入缓存中:
Cache::tags(['people', 'artists'])->put('John', $john, $seconds);
Cache::tags(['people', 'authors'])->put('Anne', $anne, $seconds);
访问带有标签的缓存项
不能在没有提供用于存储值的标签的情况下访问标记的缓存项。要检索带标签的缓存项,请将相同的有序标签列表传递给 tags
方法,然后使用想要检索的键调用 get
方法:
$john = Cache::tags(['people', 'artists'])->get('John');
$anne = Cache::tags(['people', 'authors'])->get('Anne');
删除带标签的缓存项
您可以清除分配了一个标签或一组标签的所有项目。例如,此语句将删除标记为 people
、authors
或者两者都有的缓存项。因此,Anne
和 John
都将从缓存中删除:
Cache::tags(['people', 'authors'])->flush();
相比之下,此语句将仅删除带有 authors
标签的缓存值,因此将删除 Anne
,但不会影响 John
:
Cache::tags('authors')->flush();
修剪过期的缓存标签
只有当使用 Redis 作为您的应用程序的缓存驱动程序时,才需要修剪过期的缓存标签。
为了在使用Redis缓存驱动程序时正确修剪过期的缓存标签条目,应该在应用程序的 App\Console\Kernel
类中调度 Laravel 的 cache:prune-stale-tags
Artisan 命令:
$schedule->command('cache:prune-stale-tags')->hourly();
原子锁
要使用该功能,您的应用程序必须使用
memcached
、redis
、dynamodb
、database
、file
或array
缓存驱动作为应用程序的默认缓存驱动程序。此外,所有服务器必须与同一中央缓存服务器通信。
驱动程序先决条件
数据库
使用数据库缓存驱动程序时,您需要设置一个表来包含应用程序的缓存锁。下面是表的示例 Schema
声明:
Schema::create('cache_locks', function (Blueprint $table) {
$table->string('key')->primary();
$table->string('owner');
$table->integer('expiration');
});
如果您使用
cache:table
Artisan 命令创建数据库驱动程序的缓存表,则该命令创建的迁移已包含cache_locks
表的定义。
管理锁
原子锁允许处理分布式锁而不必担心竞争条件。例如,Laravel Forge 使用原子锁来确保每次只有一个远程任务在服务器上执行。您可以使用 Cache::lock
方法创建和管理锁:
use Illuminate\Support\Facades\Cache;
$lock = Cache::lock('foo', 10);
if ($lock->get()) {
// Lock acquired for 10 seconds...
$lock->release();
}
get
方法也接受一个闭包。在闭包执行后,Laravel 将自动释放锁:
Cache::lock('foo', 10)->get(function () {
// Lock acquired for 10 seconds and automatically released...
});
如果在您请求锁时锁不可用,您可以指示 Laravel 等待指定的秒数。如果在指定的时间限制内无法获取锁定,将抛出 Illuminate\Contracts\Cache\LockTimeoutException
异常:
use Illuminate\Contracts\Cache\LockTimeoutException;
$lock = Cache::lock('foo', 10);
try {
$lock->block(5);
// Lock acquired after waiting a maximum of 5 seconds...
} catch (LockTimeoutException $e) {
// Unable to acquire lock...
} finally {
$lock?->release();
}
上述示例可以通过将闭包传递给 block
方法来简化。当将闭包传递给此方法时,Laravel将尝试在指定的秒数内获取锁定,并在执行完闭包后自动释放锁定:
Cache::lock('foo', 10)->block(5, function () {
// Lock acquired after waiting a maximum of 5 seconds...
});
跨进程管理锁定
有时,您可能希望在一个进程中获取锁定,在另一个进程中释放锁定。例如,您可能在Web请求期间获取锁定,并希望在由该请求触发的排队作业结束时释放锁定。在这种场景中,您应该通过将锁的作用域" owner token" 传递给排队作业,以便作业可以使用给定的令牌重新实例化锁定。
在下面的示例中,如果成功获取锁定,我们将调度一个排队作业。此外,我们将通过锁的 owner
方法将锁的所有者令牌传递给排队作业:
$podcast = Podcast::find($id);
$lock = Cache::lock('processing', 120);
if ($lock->get()) {
ProcessPodcast::dispatch($podcast, $lock->owner());
}
在应用程序的 ProcessPodcast
作业中,我们可以使用所有者令牌恢复和释放锁定:
Cache::restoreLock('processing', $this->owner)->release();
如果要在不尊重当前所有者的情况下释放锁定,可以使用 forceRelease
方法:
Cache::lock('processing')->forceRelease();
添加自定义缓存驱动程序
编写驱动程序
要创建自定义缓存驱动程序,首先需要实现 Illuminate\Contracts\Cache\Store
契约。因此,MongoDB 缓存实现可能如下所示:
<?php
namespace App\Extensions;
use Illuminate\Contracts\Cache\Store;
class MongoStore implements Store
{
public function get($key) {}
public function many(array $keys) {}
public function put($key, $value, $seconds) {}
public function putMany(array $values, $seconds) {}
public function increment($key, $value = 1) {}
public function decrement($key, $value = 1) {}
public function forever($key, $value) {}
public function forget($key) {}
public function flush() {}
public function getPrefix() {}
}
我们只需要使用 MongoDB 连接实现这些方法。要了解如何实现这些方法的示例,请查看 Laravel 框架源代码中的 Illuminate\Cache\MemcachedStore
。完成我们的自定义驱动程序注册后,我们可以通过调用 Cache
门面的extend
方法完成:
Cache::extend('mongo', function (Application $app) {
return Cache::repository(new MongoStore);
});
如果您想知道将自定义缓存驱动程序代码放在哪里,您可以在您的 app
目录中创建一个 Extensions
命名空间。但是,请记住,Laravel 没有严格的应用程序结构,您可以根据自己的喜好组织应用程序。
注册驱动程序
要在 Laravel 中注册自定义缓存驱动程序,我们将使用 Cache
门面的 extend
方法。由于其他服务提供程序可能尝试在其 boot
方法中读取缓存的值,我们将在引导回调中注册自定义驱动程序。通过使用引导回调,我们可以确保在我们的应用程序服务提供程序的 boot
方法被调用之前注册自定义驱动程序,但在所有服务提供程序的register
方法调用之后调用。我们将在应用程序的 App\Providers\AppServiceProvider
类的 register
方法中注册我们的引导回调:
<?php
namespace App\Providers;
use App\Extensions\MongoStore;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
$this->app->booting(function () {
Cache::extend('mongo', function (Application $app) {
return Cache::repository(new MongoStore);
});
});
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
// ...
}
}
extend
方法的第一个参数是驱动程序的名称。这将对应于 config/cache.php
配置文件中的 driver
选项。第二个参数是应返回一个 Illuminate\Cache\Repository
实例的闭包。闭包将接收一个 $app
实例,该实例是服务容器的实例。
注册扩展后,更新 config/cache.php
配置文件的 driver
选项为您的扩展名。
事件
要在每个缓存操作上执行代码,您可以侦听缓存触发的事件。通常,您应将这些事件侦听器放置在应用程序的App\Providers\EventServiceProvider
类中:
use App\Listeners\LogCacheHit;
use App\Listeners\LogCacheMissed;
use App\Listeners\LogKeyForgotten;
use App\Listeners\LogKeyWritten;
use Illuminate\Cache\Events\CacheHit;
use Illuminate\Cache\Events\CacheMissed;
use Illuminate\Cache\Events\KeyForgotten;
use Illuminate\Cache\Events\KeyWritten;
/**
* The event listener mappings for the application.
*
* @var array
*/
protected $listen = [
CacheHit::class => [
LogCacheHit::class,
],
CacheMissed::class => [
LogCacheMissed::class,
],
KeyForgotten::class => [
LogKeyForgotten::class,
],
KeyWritten::class => [
LogKeyWritten::class,
],
];
No Comments