缓存

简介

应用中有些数据查询或处理任务可能很耗 CPU,或者需要几秒才能完成。这种情况下,通常会把查到的数据缓存一段时间,这样后续请求相同数据时就能快速返回。缓存数据一般存在 Memcached 或 Redis 这类高速数据存储中。

Laravel 为各种缓存后端提供了统一、简洁的 API,让你能利用它们的高速数据读取能力来加速应用。

配置

应用的缓存配置文件在 config/cache.php。在这个文件里,你可以指定应用默认使用哪个缓存存储。Laravel 开箱支持 Memcached、Redis、DynamoDB、关系型数据库和文件系统磁盘等多种缓存后端。此外还提供了基于文件的缓存驱动,以及 array 和 null 驱动,方便你在自动化测试中使用。

缓存配置文件中还包含很多其他选项可供查看。默认情况下,Laravel 使用 database 缓存驱动,将序列化后的缓存对象存储在应用的数据库中。

驱动前提条件

数据库

使用 database 缓存驱动时,你需要一张数据库表来存放缓存数据。通常,Laravel 默认的 0001_01_01_000001_create_cache_table.php 数据库迁移已经包含了这张表。如果你的应用没有这个迁移,可以用 make:cache-table Artisan 命令创建:

php artisan make:cache-table

php artisan migrate

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,
        ],
    ],
],

如果需要,你可以把 host 选项设为 UNIX socket 路径。这种情况下,port 选项应设为 0:

'memcached' => [
    // ...

    'servers' => [
        [
            'host' => '/var/run/memcached/memcached.sock',
            'port' => 0,
            'weight' => 100
        ],
    ],
],

Redis

在 Laravel 中使用 Redis 缓存之前,你需要通过 PECL 安装 PhpRedis PHP 扩展,或者通过 Composer 安装 predis/predis 包(~2.0)。Laravel Sail 已经包含了这个扩展。此外,Laravel Cloud 和 Laravel Forge 等官方应用平台默认也安装了 PhpRedis 扩展。

有关 Redis 的更多配置信息,请查阅 Redis 文档。

文件存储

storage 缓存驱动允许你将缓存值存储在应用配置的任何文件系统磁盘上。当你想用现有的磁盘(比如 S3 磁盘)作为键值缓存存储时,这很有用:

'storage' => [
    'driver' => 'storage',
    'disk' => env('CACHE_STORAGE_DISK'),
    'path' => env('CACHE_STORAGE_PATH', 'framework/cache/data'),
],

DynamoDB

使用 DynamoDB 缓存驱动之前,你必须创建一个 DynamoDB 表来存储所有缓存数据。通常这个表应该命名为 cache。不过,你应该根据 cache 配置文件中 stores.dynamodb.table 配置项的值来命名表。表名也可以通过 DYNAMODB_CACHE_TABLE 环境变量设置。

这个表还应有一个字符串分区键,名称与应用 cache 配置文件中 stores.dynamodb.attributes.key 配置项的值对应。默认情况下,分区键名为 key。

通常,DynamoDB 不会主动从表中删除过期项。因此,你应该在表上启用生存时间(TTL)。配置表的 TTL 设置时,TTL 属性名应设为 expires_at。

接下来,安装 AWS SDK,让 Laravel 应用能和 DynamoDB 通信:

composer require aws/aws-sdk-php

此外,你应确保为 DynamoDB 缓存存储配置选项提供了值。通常这些选项(如 AWS_ACCESS_KEY_ID 和 AWS_SECRET_ACCESS_KEY)应该在应用的 .env 配置文件中定义:

'dynamodb' => [
    'driver' => 'dynamodb',
    'key' => env('AWS_ACCESS_KEY_ID'),
    'secret' => env('AWS_SECRET_ACCESS_KEY'),
    'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
    'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
    'endpoint' => env('DYNAMODB_ENDPOINT'),
],

MongoDB

如果你使用 MongoDB,官方的 mongodb/laravel-mongodb 包提供了一个 mongodb 缓存驱动,可以使用 mongodb 数据库连接进行配置。MongoDB 支持 TTL 索引,可以用来自动清除过期的缓存项。

有关 MongoDB 的更多配置信息,请查阅 MongoDB 的缓存和锁文档。

缓存使用

获取缓存实例

要获取缓存存储实例,可以使用 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() 方法的 key 应该对应 cache 配置文件中 stores 配置数组里列出的某个存储:

$value = Cache::store('file')->get('foo');

Cache::store('redis')->put('bar', 'baz', 600); // 10 分钟

从缓存中获取数据

Cache 门面的 get() 方法用于从缓存中获取数据。如果缓存中不存在该项,返回 null。你可以传第二个参数指定默认值,当缓存项不存在时返回它:

$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() 方法可以调整缓存中整数项的值。这两个方法都接受一个可选的第二参数,表示递增或递减的幅度:

// 如果值不存在则初始化...
Cache::add('key', 0, now()->plus(hours: 4));

// 递增或递减...
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() 方法的闭包会被执行,其结果会存入缓存。

如果你需要知道数据是从缓存取的还是执行闭包算出来的,可以用 rememberWithWarmth() 方法。这个方法返回一个数组,包含缓存值和一个布尔值,表示该项是否为「热」的——即从缓存中取到而非通过闭包计算:

[$value, $warm] = Cache::rememberWithWarmth('users', $seconds, function () {
    return DB::table('users')->get();
});

你可以用 rememberForever() 方法从缓存中获取项,如果不存在则永久存储:

$value = Cache::rememberForever('users', function () {
    return DB::table('users')->get();
});

过期时重新验证

使用 Cache::remember() 方法时,如果缓存值已过期,部分用户可能会遇到响应变慢。对于某些类型的数据,允许在后台重新计算缓存值的同时对外提供略微过期的数据,可以避免部分用户在缓存值被重新计算时体验到慢响应。这通常被称为「stale-while-revalidate」模式,Cache::flexible() 方法提供了这个模式的实现。

flexible() 方法接受一个数组,指定缓存值被视为「新鲜」的时长和何时变为「过期」。数组中第一个值表示缓存被视为新鲜的秒数,第二个值定义在必须重新计算之前,它可以作为过期数据被提供的秒数。

如果在新鲜期内(第一个值之前)请求,缓存会立即返回而不重新计算。如果在过期期内(两个值之间)请求,过期的值会返回给用户,同时注册一个延迟函数在响应发送给用户后刷新缓存值。如果在第二个值之后请求,缓存被视为过期,值会立即重新计算,这可能导致用户响应变慢:

$value = Cache::flexible('users', [5, 10], function () {
    return DB::table('users')->get();
});

获取并删除

如果你需要从缓存中获取一个项然后立即删除它,可以用 pull() 方法。和 get() 方法一样,如果缓存中不存在该项,返回 null:

$value = Cache::pull('key');

$value = Cache::pull('key', 'default');

在缓存中存储数据

你可以用 Cache 门面的 put() 方法在缓存中存储数据:

Cache::put('key', 'value', $seconds = 10);

如果不传存储时间,该项会无限期存储:

Cache::put('key', 'value');

除了传整数秒数,你还可以传一个 DateTime 实例表示缓存项的期望过期时间:

Cache::put('key', 'value', now()->plus(minutes: 10));

不存在时才存储

add() 方法只在缓存中不存在该项时才添加。如果项确实被添加到缓存中,方法返回 true;否则返回 false。add() 方法是一个原子操作:

Cache::add('key', 'value', $seconds);

延长缓存项寿命

touch() 方法可以延长已有缓存项的生存时间(TTL)。如果缓存项存在且过期时间成功延长,方法返回 true;如果缓存中不存在该项,返回 false:

Cache::touch('key', 3600);

你可以传 DateTimeInterface、DateInterval 或 Carbon 实例来指定精确的过期时间:

Cache::touch('key', now()->addHours(2));

永久存储

forever() 方法可以永久地在缓存中存储一个项。由于这些项不会过期,你必须用 forget() 方法手动从缓存中移除:

Cache::forever('key', 'value');

信息

如果你使用 Memcached 驱动,「永久」存储的项在缓存达到大小限制时可能会被移除。

从缓存中移除数据

你可以用 forget() 方法从缓存中移除项:

Cache::forget('key');

你也可以通过提供零或负数的过期秒数来移除项:

Cache::put('key', 'value', 0);

Cache::put('key', 'value', -5);

你可以用 flush() 方法清空整个缓存:

Cache::flush();

你可以用 flushLocks() 方法清除缓存中的所有原子锁:

Cache::flushLocks();

注意

清空缓存不会考虑你配置的缓存「前缀」,会移除缓存中的所有条目。在清空被其他应用共享的缓存时,请谨慎考虑。

缓存记忆化

Laravel 的 memo 缓存驱动允许你在单次请求或任务执行期间,将已解析的缓存值临时存储在内存中。这避免了同一次执行中重复命中缓存,能显著提升性能。

要使用记忆化缓存,调用 memo() 方法:

use Illuminate\Support\Facades\Cache;

$value = Cache::memo()->get('key');

memo() 方法可选接受一个缓存存储名称,指定记忆化驱动要装饰的底层缓存存储:

// 使用默认缓存存储...
$value = Cache::memo()->get('key');

// 使用 Redis 缓存存储...
$value = Cache::memo('redis')->get('key');

对于给定 key,第一次 get 调用会从缓存存储中获取值,但同一请求或任务中的后续调用会从内存中获取:

// 命中缓存...
$value = Cache::memo()->get('key');

// 不命中缓存,返回记忆化的值...
$value = Cache::memo()->get('key');

当调用修改缓存值的方法(如 put、increment、remember 等)时,记忆化缓存会自动遗忘已记忆的值,并将修改操作委托给底层缓存存储:

Cache::memo()->put('name', 'Taylor'); // 写入底层缓存...
Cache::memo()->get('name');           // 命中底层缓存...
Cache::memo()->get('name');           // 记忆化,不命中缓存...

Cache::memo()->put('name', 'Tim');    // 遗忘记忆化的值,写入新值...
Cache::memo()->get('name');           // 再次命中底层缓存...

缓存辅助函数

除了使用 Cache 门面,你还可以用全局 cache() 函数通过缓存获取和存储数据。当 cache() 函数以单个字符串参数调用时,返回给定 key 的值:

$value = cache('key');

如果你提供一个键值对数组和一个过期时间,函数会在缓存中存储指定时长的值:

cache(['key' => 'value'], $seconds);

cache(['key' => 'value'], now()->plus(minutes: 10));

当 cache() 函数不带任何参数调用时,返回一个 Illuminate\Contracts\Cache\Factory 实现的实例,让你可以调用其他缓存方法:

cache()->remember('users', $seconds, function () {
    return DB::table('users')->get();
});

信息

在测试全局 cache() 函数的调用时,你可以像测试门面一样使用 Cache::shouldReceive 方法。

缓存标签

注意

使用 file、dynamodb、database 或 storage 缓存驱动时不支持缓存标签。

存储带标签的缓存项

缓存标签允许你给缓存中的相关项打标签,然后可以清空分配了给定标签的所有缓存值。你可以传入一个有序的标签名数组来访问带标签的缓存。例如,让我们访问一个带标签的缓存并 put 一个值:

use Illuminate\Support\Facades\Cache;

Cache::tags(['people', 'artists'])->put('John', $john, $seconds);
Cache::tags(['people', 'authors'])->put('Anne', $anne, $seconds);

访问带标签的缓存项

通过标签存储的项不能在不提供存储时使用的标签的情况下访问。要检索带标签的缓存项,传入相同的有序标签列表给 tags() 方法,然后用你想检索的 key 调用 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();

原子锁

注意

要使用此功能,你的应用必须使用 memcached、redis、dynamodb、database、file 或 array 缓存驱动作为默认缓存驱动。此外,所有服务器必须与同一个中央缓存服务器通信。

管理锁

原子锁允许你操作分布式锁而不用担心竞态条件。例如,Laravel Cloud 使用原子锁确保同一时间只有一个远程任务在服务器上执行。你可以用 Cache::lock() 方法创建和管理锁:

use Illuminate\Support\Facades\Cache;

$lock = Cache::lock('foo', 10);

if ($lock->get()) {
    // 获取了 10 秒的锁...

    $lock->release();
}

get() 方法也接受一个闭包。闭包执行完后,Laravel 会自动释放锁:

Cache::lock('foo', 10)->get(function () {
    // 获取了 10 秒的锁并自动释放...
});

如果请求时锁不可用,你可以让 Laravel 等待指定的秒数。如果在指定时间内无法获取锁,会抛出 Illuminate\Contracts\Cache\LockTimeoutException:

use Illuminate\Contracts\Cache\LockTimeoutException;

$lock = Cache::lock('foo', 10);

try {
    $lock->block(5);

    // 等待最多 5 秒后获取了锁...
} catch (LockTimeoutException $e) {
    // 无法获取锁...
} finally {
    $lock->release();
}

上面的示例可以通过给 block() 方法传闭包来简化。传闭包时,Laravel 会尝试在指定秒数内获取锁,闭包执行完后自动释放:

Cache::lock('foo', 10)->block(5, function () {
    // 等待最多 5 秒后获取了 10 秒的锁...
});

跨进程管理锁

有时你可能想在一个进程中获取锁,在另一个进程中释放。例如,你可能在一个 Web 请求中获取锁,希望在该请求触发的队列任务结束时释放锁。在这种情况下,你应该将锁的作用域「owner token」传给队列任务,让任务能用给定 token 重新实例化锁。

在下面的示例中,如果成功获取锁,我们会分发一个队列任务。此外,我们通过锁的 owner() 方法将锁的 owner token 传给队列任务:

$podcast = Podcast::find($id);

$lock = Cache::lock('processing', 120);

if ($lock->get()) {
    ProcessPodcast::dispatch($podcast, $lock->owner());
}

在应用的 ProcessPodcast 任务中,我们可以用 owner token 恢复并释放锁:

Cache::restoreLock('processing', $this->owner)->release();

如果你想释放锁而不考虑其当前 owner,可以用 forceRelease() 方法:

Cache::lock('processing')->forceRelease();

刷新锁

如果你需要延长当前持有的锁的过期时间,可以用 refresh() 方法。如果不传秒数,会使用锁的原始时长。这对于长时间运行的操作很有用——你更愿意获取一个短锁并定期延长它,而不是获取一个过期时间很长的锁:

$lock = Cache::lock('generate-reports', 60);

if ($lock->get()) {
    foreach ($reports as $report) {
        $report->generate();

        // 延长锁 60 秒...
        $lock->refresh();
    }

    $lock->release();
}

并发限制

Laravel 的原子锁功能还提供了几种限制闭包并发执行的方式。当你想在整个基础设施中只允许一个运行实例时,使用 withoutOverlapping():

Cache::withoutOverlapping('foo', function () {
    // 等待最多 10 秒后获取了锁...
});

默认情况下,锁会持有到闭包执行完毕,方法会等待最多 10 秒来获取锁。你可以用额外的参数自定义这些值:

Cache::withoutOverlapping('foo', function () {
    // 等待最多 5 秒后获取了 120 秒的锁...
}, lockFor: 120, waitFor: 5);

如果在指定等待时间内无法获取锁,会抛出 Illuminate\Contracts\Cache\LockTimeoutException。

如果你想要可控的并行度,用 funnel() 方法设置最大并发执行数。funnel() 方法适用于任何支持锁的缓存驱动:

Cache::funnel('foo')
    ->limit(3)
    ->releaseAfter(60)
    ->block(10)
    ->then(function () {
        // 获取了并发锁...
    }, function () {
        // 无法获取并发锁...
    });

funnel 的 key 标识被限制的资源。limit() 方法定义最大并发执行数。releaseAfter() 方法设置安全超时秒数,超过后已获取的槽位会自动释放。block() 方法设置等待可用槽位的秒数。

如果你更愿意通过异常来处理超时而不是提供失败闭包,可以省略第二个闭包。如果在指定等待时间内无法获取锁,会抛出 Illuminate\Cache\Limiters\LimiterTimeoutException:

use Illuminate\Cache\Limiters\LimiterTimeoutException;

try {
    Cache::funnel('foo')
        ->limit(3)
        ->releaseAfter(60)
        ->block(10)
        ->then(function () {
            // 获取了并发锁...
        });
} catch (LimiterTimeoutException $e) {
    // 无法获取并发锁...
}

如果你想为并发限制器使用特定的缓存存储,可以在所需的存储上调用 funnel() 方法:

Cache::store('redis')->funnel('foo')
    ->limit(3)
    ->block(10)
    ->then(function () {
        // 使用 redis 存储获取了并发锁...
    });

信息

funnel() 方法要求缓存存储实现 Illuminate\Contracts\Cache\LockProvider 接口。如果你在不支持锁的缓存存储上使用 funnel(),会抛出 BadMethodCallException。

缓存故障转移

failover 缓存驱动提供了与缓存交互时的自动故障转移功能。如果 failover 存储的主缓存存储因任何原因失败,Laravel 会自动尝试使用列表中配置的下一个存储。这对于在生产环境中确保缓存可靠性的高可用性特别有用。

要配置故障转移缓存存储,指定 failover 驱动并提供一个按顺序尝试的存储名称数组。默认情况下,Laravel 在应用的 config/cache.php 配置文件中包含了一个故障转移配置示例:

'failover' => [
    'driver' => 'failover',
    'stores' => [
        'database',
        'array',
    ],
],

配置好使用 failover 驱动的存储后,你需要在应用的 .env 文件中将故障转移存储设为默认缓存存储才能使用故障转移功能:

CACHE_STORE=failover

当缓存存储操作失败并触发故障转移时,Laravel 会分发 Illuminate\Cache\Events\CacheFailedOver 事件,让你可以报告或记录缓存存储已失败。

添加自定义缓存驱动

编写驱动

要创建自定义缓存驱动,首先需要实现 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 方法中读取缓存值,我们将在 booting 回调中注册自定义驱动。通过使用 booting 回调,我们可以确保自定义驱动在应用服务提供者的 boot 方法被调用之前注册,但在所有服务提供者的 register 方法被调用之后。我们在 App\Providers\AppServiceProvider 类的 register() 方法中注册 booting 回调:

<?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 配置文件中的 CACHE_STORE 环境变量或 default 选项更新为你的扩展名称。

事件

要在每次缓存操作时执行代码,你可以监听缓存分发的各种事件:

事件名
Illuminate\Cache\Events\CacheFlushed
Illuminate\Cache\Events\CacheFlushing
Illuminate\Cache\Events\CacheFlushFailed
Illuminate\Cache\Events\CacheLocksFlushed
Illuminate\Cache\Events\CacheLocksFlushing
Illuminate\Cache\Events\CacheLocksFlushFailed
Illuminate\Cache\Events\CacheHit
Illuminate\Cache\Events\CacheMissed
Illuminate\Cache\Events\ForgettingKey
Illuminate\Cache\Events\KeyForgetFailed
Illuminate\Cache\Events\KeyForgotten
Illuminate\Cache\Events\KeyWriteFailed
Illuminate\Cache\Events\KeyWritten
Illuminate\Cache\Events\RetrievingKey
Illuminate\Cache\Events\RetrievingManyKeys
Illuminate\Cache\Events\WritingKey
Illuminate\Cache\Events\WritingManyKeys

为了提高性能,你可以在应用的 config/cache.php 配置文件中将给定缓存存储的 events 配置选项设为 false 来禁用缓存事件:

'database' => [
    'driver' => 'database',
    // ...
    'events' => false,
],