快取
簡介
有些取得資料或處理任務的過程可能很消耗 CPU、或是需要數秒鐘來完成。這種時候,我們通常會將取得的資料快取住一段時間,這樣一來在接下來的請求上就能快速存取相同的資料。快取的資料通常會初存在一些非常快速的資料儲存上,如 Memcached 或 Redis。
所幸,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 ($table) {2 $table->string('key')->unique();3 $table->text('value');4 $table->integer('expiration');5});
1Schema::create('cache', function ($table) {2 $table->string('key')->unique();3 $table->text('value');4 $table->integer('expiration');5});
可以使用 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' => 1006 ],7],
1'memcached' => [2 [3 'host' => '/var/run/memcached/memcached.sock',4 'port' => 0,5 'weight' => 1006 ],7],
Redis
在 Laravel 內使用 Redis 快取前,必須先通過 PECL 安裝 PhpRedis PHP 擴充套件,或是通過 Composer 安裝 predis/predis
套件 (~1.0)。Laravel Sail 已內建了該擴充套件。此外,官方的 Laravel 部署平台,如 Laravel Forge 與 Laravel 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<?php23namespace App\Http\Controllers;45use Illuminate\Support\Facades\Cache;67class UserController extends Controller8{9 /**10 * Show a list of all users of the application.11 *12 * @return Response13 */14 public function index()15 {16 $value = Cache::get('key');1718 //19 }20}
1<?php23namespace App\Http\Controllers;45use Illuminate\Support\Facades\Cache;67class UserController extends Controller8{9 /**10 * Show a list of all users of the application.11 *12 * @return Response13 */14 public function index()15 {16 $value = Cache::get('key');1718 //19 }20}
存取多個快取儲存
使用 Cache
Facade,即可通過 store
方法來存取多個快取儲存。傳入給 store
方法的索引鍵應對應於列在 cache
設定檔中 stores
設定的索引鍵名稱:
1$value = Cache::store('file')->get('foo');23Cache::store('redis')->put('bar', 'baz', 600); // 10 Minutes
1$value = Cache::store('file')->get('foo');23Cache::store('redis')->put('bar', 'baz', 600); // 10 Minutes
自快取內取得項目
Cache
Facade 的 get
方法是用來從快取內取得資料的。若該項目不存在於快取內,則會回傳 null
。若有需要,可以傳入第二個引數給 get
來指定項目不存在時要回傳什麼預設值:
1$value = Cache::get('key');23$value = Cache::get('key', 'default');
1$value = Cache::get('key');23$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
。否則,該方法會回傳 false
。add
方法是一個不可部分完成的操作(Atomic):
1Cache::add('key', 'value', $seconds);
1Cache::add('key', 'value', $seconds);
永久儲存項目
forever
方法可用來將項目永久儲存於快取。由於這些項目永遠不會過期,因此這些項目必須手動使用 forget
方法來移除:
1Cache::forever('key', 'value');
1Cache::forever('key', 'value');
若使用 Memcached Driver,使用「forever」儲存的項目可能會在快取達到大小限制時被移除。
從快取內取得項目
可以使用 forget
方法來自快取內移除項目:
1Cache::forget('key');
1Cache::forget('key');
也可以提供 0 或負數的過期時間來移除項目:
1Cache::put('key', 'value', 0);23Cache::put('key', 'value', -5);
1Cache::put('key', 'value', 0);23Cache::put('key', 'value', -5);
可以使用 flush
方法來移除整個快取:
1Cache::flush();
1Cache::flush();
使用 Flush 移除快取並不理會所設定的快取「前綴」,會將快取內所有的項目都移除。當快取有與其他應用程式共用時,在清除快取前請三思。
Cache 輔助函式
除了使用 Cache
Facade,也可以使用全域的 cache
函式來自快取內取得與儲存資料。當使用單一的字串引數呼叫 cache
方法時,會回傳給定索引鍵的值:
1$value = cache('key');
1$value = cache('key');
若傳入一組索引鍵/值配對的陣列以及一個過期時間給該函式,則會將數值初存在快取內一段給定的期間:
1cache(['key' => 'value'], $seconds);23cache(['key' => 'value'], now()->addMinutes(10));
1cache(['key' => 'value'], $seconds);23cache(['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});
在測試呼叫全域的 cache
函式時,可以像在測試 Facade一樣,使用 Cache::shouldReceive
方法。
快取標籤
使用 file
, dynamodb
或 database
快取 Driver 時,不支援使用快取標籤。此外,在以「forever」儲存的快取上使用多重標籤時,搭配 memcached
Driver 能取得最佳效能,這些 Driver 通常會自動移除舊的記錄。
儲存標籤的快取項目
快取標籤能讓你將快取內相關的項目標記在一起,並能將所有被指派到相同標籤的快取值一起被清除。可以通過傳入包含標籤名稱的有序陣列來存取標籤快取。舉例來說,我們來存取一個被標籤的快取,並將一個值 put
進快取內:
1Cache::tags(['people', 'artists'])->put('John', $john, $seconds);23Cache::tags(['people', 'authors'])->put('Anne', $anne, $seconds);
1Cache::tags(['people', 'artists'])->put('John', $john, $seconds);23Cache::tags(['people', 'authors'])->put('Anne', $anne, $seconds);
存取標籤的快取項目
若要取得標籤的快取項目,可傳入相同的包含標籤的有序列表至 tags
方法內,並接著以欲存取的索引鍵來呼叫 get
方法:
1$john = Cache::tags(['people', 'artists'])->get('John');23$anne = Cache::tags(['people', 'authors'])->get('Anne');
1$john = Cache::tags(['people', 'artists'])->get('John');23$anne = Cache::tags(['people', 'authors'])->get('Anne');
移除標籤的快取項目
可以移除有被設定一個或多個標籤的項目。舉例來說,這個陳述式可以移除所有被設為 people
、authors
、或是同時有這兩個標籤的快取。因此,Anne
與 John
都會被從快取內移除:
1Cache::tags(['people', 'authors'])->flush();
1Cache::tags(['people', 'authors'])->flush();
與之相比,下列這個陳述式只會移除被標記為 authors
的快取值,因此 Anne
會被移除,而 John
則不會:
1Cache::tags('authors')->flush();
1Cache::tags('authors')->flush();
Atomic Lock (不可部分完成的鎖定)
若要使用此功能,則應用程式必須要使用 memcached
, redis
, dynamodb
, database
, file
或 array
作為應用程式的預設快取 Driver。另外,所有的伺服器也都必須要連線至相同的中央快取伺服器。
Driver 需求
資料庫
在使用 database
快取 Driver 時,需要設定包含專案快取 Lock 的資料表。下列為範例的資料表 Schema
宣告:
1Schema::create('cache_locks', function ($table) {2 $table->string('key')->primary();3 $table->string('owner');4 $table->integer('expiration');5});
1Schema::create('cache_locks', function ($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;23$lock = Cache::lock('foo', 10);45if ($lock->get()) {6 // Lock acquired for 10 seconds...78 $lock->release();9}
1use Illuminate\Support\Facades\Cache;23$lock = Cache::lock('foo', 10);45if ($lock->get()) {6 // Lock acquired for 10 seconds...78 $lock->release();9}
get
方法也接收一個閉包。在該閉包執行後,Laravel 會自動釋放 Lock:
1Cache::lock('foo')->get(function () {2 // 立刻要求 Lock 並自動釋放……3});
1Cache::lock('foo')->get(function () {2 // 立刻要求 Lock 並自動釋放……3});
若在要求時無法取得 Lock,則可以告訴 Laravel 要等待多少秒的事件。若在指定的時間限制後仍無法取得 Lock,則會擲回 Illuminate\Contracts\Cache\LockTimeoutException
:
1use Illuminate\Contracts\Cache\LockTimeoutException;23$lock = Cache::lock('foo', 10);45try {6 $lock->block(5);78 // 等待最多 5 秒取得 Lock...9} catch (LockTimeoutException $e) {10 // 無法取得 Lock...11} finally {12 optional($lock)->release();13}
1use Illuminate\Contracts\Cache\LockTimeoutException;23$lock = Cache::lock('foo', 10);45try {6 $lock->block(5);78 // 等待最多 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);23$lock = Cache::lock('processing', 120);45if ($lock->get()) {6 ProcessPodcast::dispatch($podcast, $lock->owner());7}
1$podcast = Podcast::find($id);23$lock = Cache::lock('processing', 120);45if ($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<?php23namespace App\Extensions;45use Illuminate\Contracts\Cache\Store;67class MongoStore implements Store8{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<?php23namespace App\Extensions;45use Illuminate\Contracts\Cache\Store;67class MongoStore implements Store8{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 ($app) {2 return Cache::repository(new MongoStore);3});
1Cache::extend('mongo', function ($app) {2 return Cache::repository(new MongoStore);3});
若不知道該將自定快取 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<?php23namespace App\Providers;45use App\Extensions\MongoStore;6use Illuminate\Support\Facades\Cache;7use Illuminate\Support\ServiceProvider;89class CacheServiceProvider extends ServiceProvider10{11 /**12 * Register any application services.13 *14 * @return void15 */16 public function register()17 {18 $this->app->booting(function () {19 Cache::extend('mongo', function ($app) {20 return Cache::repository(new MongoStore);21 });22 });23 }2425 /**26 * Bootstrap any application services.27 *28 * @return void29 */30 public function boot()31 {32 //33 }34}
1<?php23namespace App\Providers;45use App\Extensions\MongoStore;6use Illuminate\Support\Facades\Cache;7use Illuminate\Support\ServiceProvider;89class CacheServiceProvider extends ServiceProvider10{11 /**12 * Register any application services.13 *14 * @return void15 */16 public function register()17 {18 $this->app->booting(function () {19 Cache::extend('mongo', function ($app) {20 return Cache::repository(new MongoStore);21 });22 });23 }2425 /**26 * Bootstrap any application services.27 *28 * @return void29 */30 public function boot()31 {32 //33 }34}
傳入 extend
方法的第一個引數為 Driver 的名稱。這個名稱應對應到 config/cache.php
設定檔中的 driver
選項。第二個引數則是一個應回傳 Illuminate\Cache\Repository
實體的閉包。該閉包會被傳入一個 $app
實體,即為 Service Container 的實體。
註冊好擴充程式後,就可以將 config/cache.php
設定檔中的 driver
選項更新為擴充程式的名稱。
事件
若要在每個快取操作時執行程式碼,可以監聽快取所觸發的事件。一般來說,這些事件監聽程式應放置於專案的 App\Providers\EventServiceProvider
類別:
1/**2 * The event listener mappings for the application.3 *4 * @var array5 */6protected $listen = [7 'Illuminate\Cache\Events\CacheHit' => [8 'App\Listeners\LogCacheHit',9 ],1011 'Illuminate\Cache\Events\CacheMissed' => [12 'App\Listeners\LogCacheMissed',13 ],1415 'Illuminate\Cache\Events\KeyForgotten' => [16 'App\Listeners\LogKeyForgotten',17 ],1819 'Illuminate\Cache\Events\KeyWritten' => [20 'App\Listeners\LogKeyWritten',21 ],22];
1/**2 * The event listener mappings for the application.3 *4 * @var array5 */6protected $listen = [7 'Illuminate\Cache\Events\CacheHit' => [8 'App\Listeners\LogCacheHit',9 ],1011 'Illuminate\Cache\Events\CacheMissed' => [12 'App\Listeners\LogCacheMissed',13 ],1415 'Illuminate\Cache\Events\KeyForgotten' => [16 'App\Listeners\LogKeyForgotten',17 ],1819 'Illuminate\Cache\Events\KeyWritten' => [20 'App\Listeners\LogKeyWritten',21 ],22];