快取

簡介

有些取得資料或處理任務的過程可能很消耗 CPU、或是需要數秒鐘來完成。這種時候,我們通常會將取得的資料快取住一段時間,這樣一來在接下來的請求上就能快速存取相同的資料。快取的資料通常會初存在一些非常快速的資料儲存上,如 MemcachedRedis

所幸,Laravel 為多種快取後端提供了一個表達性、統一的 API,可以享受快取提供的快速資料存取,並加速你的網站。

設定

快取設定檔位於 config/cache.php。在這個檔案中,可以指定專案中預設要使用哪個快取 Driver。Laravel 內建支援像是 Memcached, Redis, DynamoDB 以及關聯式資料庫等多種熱門的快取後端。此外,也可以使用基於檔案的快取 Driver,而 array 與「null」Driver 則為自動化測試提供方便的快取後端。

快取設定檔也包含了其他數種選項,並在該設定檔中包含了說明文件。請確保有先閱讀這些選項。預設情況下,Laravel 設定使用 file 快取 Driver,在伺服器的檔案系統上儲存經過序列化的快取物件。對於大型的專案,建議使用如 Memcached 或 Redis 等更專門的快取 Driver。甚至也可以為相同的 Driver 設定多個快取設定。

Driver 需求

資料庫

在使用 database 快取 Driver 時,需要先設定包含快取項目的資料表。該資料表的 Schema 宣告範例如下:

1Schema::create('cache', function (Blueprint $table) {
2 $table->string('key')->unique();
3 $table->text('value');
4 $table->integer('expiration');
5});
1Schema::create('cache', function (Blueprint $table) {
2 $table->string('key')->unique();
3 $table->text('value');
4 $table->integer('expiration');
5});
lightbulb

可以使用 php artisan cache:table Artisan 指令來產生包含正確 Schema 的 Migration。

Memcached

要使用 Memcached Driver 需要安裝 Memcached PECL 套件。可以在 config/cache.php 設定檔中列出所有的 Memcached 伺服器。這個檔案已預先包含了 memcached.servers 欄位來讓你開始使用:

1'memcached' => [
2 'servers' => [
3 [
4 'host' => env('MEMCACHED_HOST', '127.0.0.1'),
5 'port' => env('MEMCACHED_PORT', 11211),
6 'weight' => 100,
7 ],
8 ],
9],
1'memcached' => [
2 'servers' => [
3 [
4 'host' => env('MEMCACHED_HOST', '127.0.0.1'),
5 'port' => env('MEMCACHED_PORT', 11211),
6 'weight' => 100,
7 ],
8 ],
9],

若有需要,可以將 host 選項設為 UNIX Socket 路徑。若設定為 UNIX Socket,則 port 選項應設為 0

1'memcached' => [
2 [
3 'host' => '/var/run/memcached/memcached.sock',
4 'port' => 0,
5 'weight' => 100
6 ],
7],
1'memcached' => [
2 [
3 'host' => '/var/run/memcached/memcached.sock',
4 'port' => 0,
5 'weight' => 100
6 ],
7],

Redis

在 Laravel 內使用 Redis 快取前,必須先通過 PECL 安裝 PhpRedis PHP 擴充套件,或是通過 Composer 安裝 predis/predis 套件 (~1.0)。Laravel Sail 已內建了該擴充套件。此外,官方的 Laravel 部署平台,如 Laravel ForgeLaravel Vapor,都已預設安裝了 PhpRedis 擴充套件。

更多有關設定 Redis 的資訊,請參考 Laravel 說明文件頁面

DynamoDB

在開始使用 DynamoDB 快取 Driver 前,必須先建立 DynamoDB 資料表以儲存所有的快取資料。通常來說,這個資料表應命名為 cache。不過,應依照專案的 cache 設定檔中的 stores.dynamodb.table 設定值來設定這個資料表的名稱。

該資料表也應擁有一個字串 Partition Key,其名稱應對應專案的 cache 設定檔的 stores.dynamodb.attributes.key 設定值。預設情況下,該 Partition Key 應命名為 key

使用快取

取得 Cache 實體

若要取得快取儲存的實體,可以使用 Cache Facade。我們在這篇說明文件中都會使用該 Facade。Cache Facade 提供了一個方便簡潔的方式來存取 Laravel 快取 Contract 底層的實作:

1<?php
2 
3namespace App\Http\Controllers;
4 
5use Illuminate\Support\Facades\Cache;
6 
7class UserController extends Controller
8{
9 /**
10 * Show a list of all users of the application.
11 */
12 public function index(): array
13 {
14 $value = Cache::get('key');
15 
16 return [
17 // ...
18 ];
19 }
20}
1<?php
2 
3namespace App\Http\Controllers;
4 
5use Illuminate\Support\Facades\Cache;
6 
7class UserController extends Controller
8{
9 /**
10 * Show a list of all users of the application.
11 */
12 public function index(): array
13 {
14 $value = Cache::get('key');
15 
16 return [
17 // ...
18 ];
19 }
20}

存取多個快取儲存

使用 Cache Facade,即可通過 store 方法來存取多個快取儲存。傳入給 store 方法的索引鍵應對應於列在 cache 設定檔中 stores 設定的索引鍵名稱:

1$value = Cache::store('file')->get('foo');
2 
3Cache::store('redis')->put('bar', 'baz', 600); // 10 Minutes
1$value = Cache::store('file')->get('foo');
2 
3Cache::store('redis')->put('bar', 'baz', 600); // 10 Minutes

自快取內取得項目

Cache Facade 的 get 方法是用來從快取內取得資料的。若該項目不存在於快取內,則會回傳 null。若有需要,可以傳入第二個引數給 get 來指定項目不存在時要回傳什麼預設值:

1$value = Cache::get('key');
2 
3$value = Cache::get('key', 'default');
1$value = Cache::get('key');
2 
3$value = Cache::get('key', 'default');

也可以傳入一個閉包來作為預設值。若指定項目不存在於快取內,則該閉包的結果會被回傳。傳入閉包可讓你暫緩從資料庫或其他外部服務取得預設值的過程:

1$value = Cache::get('key', function () {
2 return DB::table(/* ... */)->get();
3});
1$value = Cache::get('key', function () {
2 return DB::table(/* ... */)->get();
3});

檢查項目是否存在

has 方法可以用來判斷某個項目是否存在於快取內。該方法也會在項目存在,但其值為 null 時回傳 false

1if (Cache::has('key')) {
2 // ...
3}
1if (Cache::has('key')) {
2 // ...
3}

遞增或遞減值

increment(遞增)與 decrement(遞減)方法可以用來調整快取中的整數項目值。這兩個方法都接收一個可選的第二個引數來判斷項目值所要遞增或遞減的值:

1Cache::increment('key');
2Cache::increment('key', $amount);
3Cache::decrement('key');
4Cache::decrement('key', $amount);
1Cache::increment('key');
2Cache::increment('key', $amount);
3Cache::decrement('key');
4Cache::decrement('key', $amount);

取得與儲存

有時候,我們可能會想要從快取內取得項目,但也想在項目不存在的時候設定預設值。舉例來說,我們可能想從快取內取得所有的使用者,但若快取不存在,則從資料庫內取得所有使用者,並存入快取。可以使用 Cache::remember 方法:

1$value = Cache::remember('users', $seconds, function () {
2 return DB::table('users')->get();
3});
1$value = Cache::remember('users', $seconds, function () {
2 return DB::table('users')->get();
3});

若該項目不存在於快取內,則傳入 remember 的閉包會被執行,並將其結果放入快取內。

可以使用 rememberForever 方法來從快取內取得項目,並在項目不存在時將其永久保存在快取內:

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

取得或刪除

若有需要從快取內取得並同時刪除項目,則可以使用 pull 方法。與 get 方法類似,當項目不存在於快取內時,會回傳 null

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

將項目存入快取

可以使用 Cache Facade 上的 put 方法來將項目存入快取:

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

若未傳入儲存時間給 put 方法,則該項目將被永久儲存:

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

除了將秒數作為整數傳入,也可以傳入一個 DateTime 實體來代表指定的快取項目過期時間:

1Cache::put('key', 'value', now()->addMinutes(10));
1Cache::put('key', 'value', now()->addMinutes(10));

當不存在時儲存

add 方法會只在項目不存在於快取儲存內時將項目加進快取內。該方法會在項目有真正被加進快取後回傳 true。否則,該方法會回傳 falseadd 方法是一個不可部分完成的操作(Atomic):

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

永久儲存項目

forever 方法可用來將項目永久儲存於快取。由於這些項目永遠不會過期,因此這些項目必須手動使用 forget 方法來移除:

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

若使用 Memcached Driver,使用「forever」儲存的項目可能會在快取達到大小限制時被移除。

從快取內取得項目

可以使用 forget 方法來自快取內移除項目:

1Cache::forget('key');
1Cache::forget('key');

也可以提供 0 或負數的過期時間來移除項目:

1Cache::put('key', 'value', 0);
2 
3Cache::put('key', 'value', -5);
1Cache::put('key', 'value', 0);
2 
3Cache::put('key', 'value', -5);

可以使用 flush 方法來移除整個快取:

1Cache::flush();
1Cache::flush();
exclamation

使用 Flush 移除快取並不理會所設定的快取「Prefix(前置詞)」,會將快取內所有的項目都移除。當快取有與其他應用程式共用時,在清除快取前請三思。

Cache 輔助函式

除了使用 Cache Facade,也可以使用全域的 cache 函式來自快取內取得與儲存資料。當使用單一的字串引數呼叫 cache 方法時,會回傳給定索引鍵的值:

1$value = cache('key');
1$value = cache('key');

若傳入一組索引鍵/值配對的陣列以及一個過期時間給該函式,則會將數值初存在快取內一段給定的期間:

1cache(['key' => 'value'], $seconds);
2 
3cache(['key' => 'value'], now()->addMinutes(10));
1cache(['key' => 'value'], $seconds);
2 
3cache(['key' => 'value'], now()->addMinutes(10));

cache 方法被呼叫,但未傳入任何引數時,會回傳 Illuminate\Contracts\Cache\Factory 實作的實體,可以讓你呼叫其他快取方法:

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

在測試呼叫全域的 cache 函式時,可以像在測試 Facade一樣,使用 Cache::shouldReceive 方法。

快取標籤

exclamation

使用 file, dynamodbdatabase 快取 Driver 時,不支援使用快取標籤。此外,在以「forever」儲存的快取上使用多重標籤時,搭配 memcached Driver 能取得最佳效能,這些 Driver 通常會自動移除舊的記錄。

儲存標籤的快取項目

快取標籤能讓你將快取內相關的項目標記在一起,並能將所有被指派到相同標籤的快取值一起被清除。可以通過傳入包含標籤名稱的有序陣列來存取標籤快取。舉例來說,我們來存取一個被標籤的快取,並將一個值 put 進快取內:

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

存取標籤的快取項目

若沒有提供與保存項目時所指定的相同 Tag,就無法存取這些項目。若要取得待 Tag 的快取項目,請將相同的 Tag 以相同的順序傳入給 tags 方法,然後再以指定的索引鍵來呼叫 get 方法:

1$john = Cache::tags(['people', 'artists'])->get('John');
2 
3$anne = Cache::tags(['people', 'authors'])->get('Anne');
1$john = Cache::tags(['people', 'artists'])->get('John');
2 
3$anne = Cache::tags(['people', 'authors'])->get('Anne');

移除標籤的快取項目

可以移除有被設定一個或多個標籤的項目。舉例來說,這個陳述式可以移除所有被設為 peopleauthors、或是同時有這兩個標籤的快取。因此,AnneJohn 都會被從快取內移除:

1Cache::tags(['people', 'authors'])->flush();
1Cache::tags(['people', 'authors'])->flush();

與之相比,下列這個陳述式只會移除被標記為 authors 的快取值,因此 Anne 會被移除,而 John 則不會:

1Cache::tags('authors')->flush();
1Cache::tags('authors')->flush();

修建過時的快取 Tag

exclamation

只有當專案使用 Redis 作為快取 Driver 時,才需要修建過時的快取 Tag。

在使用 Redis 快取 Driver 時,若要正確地修建過時的快取 Tag,請在專案的 App\Console\Kernel 類別內排程呼叫 Laravel 的 cache:prune-stale-tags Artisan 指令:

1$schedule->command('cache:prune-stale-tags')->hourly();
1$schedule->command('cache:prune-stale-tags')->hourly();

Atomic Lock (不可部分完成的鎖定)

exclamation

若要使用此功能,則應用程式必須要使用 memcached, redis, dynamodb, database, filearray 作為應用程式的預設快取 Driver。另外,所有的伺服器也都必須要連線至相同的中央快取伺服器。

Driver 需求

資料庫

在使用 database 快取 Driver 時,需要設定包含專案快取 Lock 的資料表。下列為範例的資料表 Schema 宣告:

1Schema::create('cache_locks', function (Blueprint $table) {
2 $table->string('key')->primary();
3 $table->string('owner');
4 $table->integer('expiration');
5});
1Schema::create('cache_locks', function (Blueprint $table) {
2 $table->string('key')->primary();
3 $table->string('owner');
4 $table->integer('expiration');
5});

管理 Lock

使用 Atomic Lock (不可部分完成鎖定),在操作與分配 Lock 時即可不需理會競爭條件 (Race Condition)。舉例來說,Laravel Forge 使用 Atomic Lock 來確保在一台伺服器上一次只有一個遠端任務在執行。可以通過 Cache::lock 方法來建立與管理 Lock:

1use Illuminate\Support\Facades\Cache;
2 
3$lock = Cache::lock('foo', 10);
4 
5if ($lock->get()) {
6 // Lock acquired for 10 seconds...
7 
8 $lock->release();
9}
1use Illuminate\Support\Facades\Cache;
2 
3$lock = Cache::lock('foo', 10);
4 
5if ($lock->get()) {
6 // Lock acquired for 10 seconds...
7 
8 $lock->release();
9}

get 方法也接收一個閉包。在該閉包執行後,Laravel 會自動釋放 Lock:

1Cache::lock('foo', 10)->get(function () {
2 // 取得 10 秒的 Lock,然後自動釋放...
3});
1Cache::lock('foo', 10)->get(function () {
2 // 取得 10 秒的 Lock,然後自動釋放...
3});

若在要求時無法取得 Lock,則可以告訴 Laravel 要等待多少秒的事件。若在指定的時間限制後仍無法取得 Lock,則會擲回 Illuminate\Contracts\Cache\LockTimeoutException

1use Illuminate\Contracts\Cache\LockTimeoutException;
2 
3$lock = Cache::lock('foo', 10);
4 
5try {
6 $lock->block(5);
7 
8 // 等待最多 5 秒取得 Lock...
9} catch (LockTimeoutException $e) {
10 // 無法取得 Lock...
11} finally {
12 optional($lock)->release();
13}
1use Illuminate\Contracts\Cache\LockTimeoutException;
2 
3$lock = Cache::lock('foo', 10);
4 
5try {
6 $lock->block(5);
7 
8 // 等待最多 5 秒取得 Lock...
9} catch (LockTimeoutException $e) {
10 // 無法取得 Lock...
11} finally {
12 optional($lock)->release();
13}

上述範例可以通過將閉包傳入 block 方法來簡化。當傳入閉包給該方法後,Laravel 會嘗試在指定秒數內取得 Lock,並在閉包執行後自動釋放 Lock:

1Cache::lock('foo', 10)->block(5, function () {
2 // 等待最多 5 秒取得 Lock…
3});
1Cache::lock('foo', 10)->block(5, function () {
2 // 等待最多 5 秒取得 Lock…
3});

在多個處理程序間管理 Lock

有的時候我們可能想要在一個處理程序內要求 Lock,並在另一個處理程序中釋放。舉例來說,我們可能會在某個網頁請求的期間內要求 Lock,並在由該請求觸發的佇列任務完成後才釋放該 Lock。在此情境中,應將該 Lock 的區域性「擁有者權杖」傳給佇列任務,以讓佇列任務可以使用給定的權杖來重新取得 Lock。

在下方的範例中,我們會在成功取得 Lock 後分派佇列任務。另外,我們也會通過 Lock 的 owner 方法來將 Lock 的擁有者權杖傳給佇列任務。

1$podcast = Podcast::find($id);
2 
3$lock = Cache::lock('processing', 120);
4 
5if ($lock->get()) {
6 ProcessPodcast::dispatch($podcast, $lock->owner());
7}
1$podcast = Podcast::find($id);
2 
3$lock = Cache::lock('processing', 120);
4 
5if ($lock->get()) {
6 ProcessPodcast::dispatch($podcast, $lock->owner());
7}

在專案的 ProcessPodcast 任務中,我們可以通過擁有者權杖來恢復與釋放 Lock:

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

若想在不理會目前擁有者的情況下釋放 Lock,可以使用 forceRelease 方法:

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

新增自訂快取 Driver

撰寫 Driver

若要建立自訂快取 Driver,首先必須實作 Illuminate\Contracts\Cache\Store Contract。因此,一個 MongoDB 的快取實作看起來會長這樣:

1<?php
2 
3namespace App\Extensions;
4 
5use Illuminate\Contracts\Cache\Store;
6 
7class MongoStore implements Store
8{
9 public function get($key) {}
10 public function many(array $keys) {}
11 public function put($key, $value, $seconds) {}
12 public function putMany(array $values, $seconds) {}
13 public function increment($key, $value = 1) {}
14 public function decrement($key, $value = 1) {}
15 public function forever($key, $value) {}
16 public function forget($key) {}
17 public function flush() {}
18 public function getPrefix() {}
19}
1<?php
2 
3namespace App\Extensions;
4 
5use Illuminate\Contracts\Cache\Store;
6 
7class MongoStore implements Store
8{
9 public function get($key) {}
10 public function many(array $keys) {}
11 public function put($key, $value, $seconds) {}
12 public function putMany(array $values, $seconds) {}
13 public function increment($key, $value = 1) {}
14 public function decrement($key, $value = 1) {}
15 public function forever($key, $value) {}
16 public function forget($key) {}
17 public function flush() {}
18 public function getPrefix() {}
19}

我們只需要通過 MongoDB 連線來實作其中的各個方法即可。有關如何實作這些方法,請參考 Laravel 框架原始碼 中的 Illuminate\Cache\MemcachedStore。實作完成後,就可以呼叫 Cache Facade 的 extend 方法來註冊自訂 Driver:

1Cache::extend('mongo', function (Application $app) {
2 return Cache::repository(new MongoStore);
3});
1Cache::extend('mongo', function (Application $app) {
2 return Cache::repository(new MongoStore);
3});
lightbulb

若不知道該將自定快取 Driver 的程式碼放在哪裡,可在 app 目錄內建立一個 Extensions 命名空間。不過,請記得,Laravel 並沒有硬性規定應用程式的架構,你可以隨意依照你的喜好來阻止程式碼。

註冊 Driver

若要向 Laravel 註冊自訂快取 Driver,可以使用 Cache Facade 上的 extend 方法。由於其他的 Service Provider 可能會嘗試在 boot 方法內讀取快取值,因此我們需要將自訂 Driver 註冊在 booting 回呼內。只要使用了 booting 回呼,就能確保自訂回呼是在其他 Service Provider 的 boot 方法被呼叫前、以及 App\Providers\AppServiceProvider 類別的 register 方法被呼叫前被註冊的。我們會將 booting 回呼放在專案的 App\Providers\AppServiceProvider 類別中的 register 方法內:

1<?php
2 
3namespace App\Providers;
4 
5use App\Extensions\MongoStore;
6use Illuminate\Contracts\Foundation\Application;
7use Illuminate\Support\Facades\Cache;
8use Illuminate\Support\ServiceProvider;
9 
10class CacheServiceProvider extends ServiceProvider
11{
12 /**
13 * Register any application services.
14 */
15 public function register(): void
16 {
17 $this->app->booting(function () {
18 Cache::extend('mongo', function (Application $app) {
19 return Cache::repository(new MongoStore);
20 });
21 });
22 }
23 
24 /**
25 * Bootstrap any application services.
26 */
27 public function boot(): void
28 {
29 // ...
30 }
31}
1<?php
2 
3namespace App\Providers;
4 
5use App\Extensions\MongoStore;
6use Illuminate\Contracts\Foundation\Application;
7use Illuminate\Support\Facades\Cache;
8use Illuminate\Support\ServiceProvider;
9 
10class CacheServiceProvider extends ServiceProvider
11{
12 /**
13 * Register any application services.
14 */
15 public function register(): void
16 {
17 $this->app->booting(function () {
18 Cache::extend('mongo', function (Application $app) {
19 return Cache::repository(new MongoStore);
20 });
21 });
22 }
23 
24 /**
25 * Bootstrap any application services.
26 */
27 public function boot(): void
28 {
29 // ...
30 }
31}

傳入 extend 方法的第一個引數為 Driver 的名稱。這個名稱應對應到 config/cache.php 設定檔中的 driver 選項。第二個引數則是一個應回傳 Illuminate\Cache\Repository 實體的閉包。該閉包會被傳入一個 $app 實體,即為 Service Container 的實體。

註冊好擴充程式後,就可以將 config/cache.php 設定檔中的 driver 選項更新為擴充程式的名稱。

事件

若要在每個快取操作時執行程式碼,可以監聽快取所觸發的事件。一般來說,這些事件監聽程式應放置於專案的 App\Providers\EventServiceProvider 類別:

1use App\Listeners\LogCacheHit;
2use App\Listeners\LogCacheMissed;
3use App\Listeners\LogKeyForgotten;
4use App\Listeners\LogKeyWritten;
5use Illuminate\Cache\Events\CacheHit;
6use Illuminate\Cache\Events\CacheMissed;
7use Illuminate\Cache\Events\KeyForgotten;
8use Illuminate\Cache\Events\KeyWritten;
9 
10/**
11 * The event listener mappings for the application.
12 *
13 * @var array
14 */
15protected $listen = [
16 CacheHit::class => [
17 LogCacheHit::class,
18 ],
19 
20 CacheMissed::class => [
21 LogCacheMissed::class,
22 ],
23 
24 KeyForgotten::class => [
25 LogKeyForgotten::class,
26 ],
27 
28 KeyWritten::class => [
29 LogKeyWritten::class,
30 ],
31];
1use App\Listeners\LogCacheHit;
2use App\Listeners\LogCacheMissed;
3use App\Listeners\LogKeyForgotten;
4use App\Listeners\LogKeyWritten;
5use Illuminate\Cache\Events\CacheHit;
6use Illuminate\Cache\Events\CacheMissed;
7use Illuminate\Cache\Events\KeyForgotten;
8use Illuminate\Cache\Events\KeyWritten;
9 
10/**
11 * The event listener mappings for the application.
12 *
13 * @var array
14 */
15protected $listen = [
16 CacheHit::class => [
17 LogCacheHit::class,
18 ],
19 
20 CacheMissed::class => [
21 LogCacheMissed::class,
22 ],
23 
24 KeyForgotten::class => [
25 LogKeyForgotten::class,
26 ],
27 
28 KeyWritten::class => [
29 LogKeyWritten::class,
30 ],
31];
翻譯進度
100% 已翻譯
更新時間:
2024年6月30日 上午8:17:00 [世界標準時間]
翻譯人員:
  • cornch
幫我們翻譯此頁

留言

尚無留言

“Laravel” is a Trademark of Taylor Otwell.
The source documentation is released under MIT license. See laravel/docs on GitHub for details.
The translated documentations are released under MIT license. See cornch/laravel-docs-l10n on GitHub for details.