#はじめに
アプリケーションで行うデータ取得や処理の中には、CPU負荷が高かったり完了までに数秒かかるものがあります。このような場合、取得したデータを一定時間キャッシュしておき、同じデータへの後続リクエストで高速に取得できるようにするのが一般的です。キャッシュされたデータは通常、Memcached や Redis のような非常に高速なデータストアに保存されます。
Laravel は様々なキャッシュバックエンドに対して表現力豊かで統一されたAPIを提供しており、それらの高速なデータ取得を活用してウェブアプリケーションのパフォーマンスを向上できます。
#設定
アプリケーションのキャッシュ設定ファイルは config/cache.php にあります。このファイルで、アプリケーション全体でデフォルトで使用するキャッシュドライバーを指定できます。Laravel は標準で Memcached、Redis、DynamoDB、リレーショナルデータベースなどの人気のあるキャッシュバックエンドをサポートしています。さらに、ファイルベースのキャッシュドライバーも利用可能で、array や "null" ドライバーは自動テスト用の便利なキャッシュバックエンドを提供します。
キャッシュ設定ファイルには他にも様々なオプションが含まれており、ファイル内で説明されていますので必ず確認してください。デフォルトでは Laravel は file キャッシュドライバーを使用するよう設定されており、これはシリアライズされたキャッシュオブジェクトをサーバーのファイルシステムに保存します。大規模なアプリケーションでは、Memcached や Redis のようなより堅牢なドライバーの使用を推奨します。同じドライバーに対して複数のキャッシュ設定を構成することも可能です。
#ドライバーの前提条件
#データベース
database キャッシュドライバーを使用する場合、キャッシュアイテムを格納するテーブルを用意する必要があります。以下にテーブルの Schema 宣言例を示します:
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,
],
],
],
必要に応じて、host オプションに UNIX ソケットのパスを指定できます。この場合、port オプションは 0 に設定してください:
'memcached' => [
[
'host' => '/var/run/memcached/memcached.sock',
'port' => 0,
'weight' => 100
],
],
#Redis
Laravel で Redis キャッシュを使う前に、PECL 経由で PhpRedis PHP拡張をインストールするか、Composer 経由で predis/predis パッケージ(~1.0)をインストールする必要があります。Laravel Sail にはこの拡張がすでに含まれています。また、Laravel Forge や Laravel Vapor といった公式のデプロイプラットフォームには PhpRedis 拡張がデフォルトでインストールされています。
Redis の設定については、Laravel の Redis ドキュメントページ を参照してください。
#DynamoDB
DynamoDB キャッシュドライバーを使う前に、キャッシュデータを保存する DynamoDB テーブルを作成する必要があります。通常、このテーブル名は cache としますが、アプリケーションの cache 設定ファイル内の stores.dynamodb.table 設定値に基づいて名前を付けるべきです。
このテーブルには、アプリケーションの cache 設定ファイル内の stores.dynamodb.attributes.key 設定値に対応する名前の文字列パーティションキーも必要です。デフォルトではパーティションキー名は key です。
#キャッシュの使い方
#キャッシュインスタンスの取得
キャッシュストアのインスタンスを取得するには、Cacheファサードを使用できます。このドキュメント全体でこれを使用します。Cacheファサードは、Laravel のキャッシュ契約の基盤となる実装に対して、便利で簡潔なアクセスを提供します:
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Facades\Cache;
class UserController extends Controller
{
/**
* アプリケーションの全ユーザー一覧を表示します。
*/
public function index(): array
{
$value = Cache::get('key');
return [
// ...
];
}
}
#複数のキャッシュストアへのアクセス
Cache ファサードを使うと、store メソッド経由で様々なキャッシュストアにアクセスできます。store メソッドに渡すキーは、cache 設定ファイルの stores 配列にリストされているストアのいずれかに対応している必要があります:
$value = Cache::store('file')->get('foo');
Cache::store('redis')->put('bar', 'baz', 600); // 10分
#キャッシュからのアイテム取得
Cache ファサードの get メソッドはキャッシュからアイテムを取得します。アイテムが存在しない場合は null を返します。必要に応じて、get メソッドの第2引数にアイテムが存在しない場合のデフォルト値を指定できます:
$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 メソッドはキャッシュ内の整数値を増減させるために使います。どちらのメソッドも第2引数で増減量を指定できます:
// 値が存在しない場合は初期化...
Cache::add('key', 0, now()->addHours(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 に渡したクロージャが実行され、その結果がキャッシュに保存されます。
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 インスタンスを渡すこともできます:
Cache::put('key', 'value', now()->addMinutes(10));
#存在しない場合のみ保存
add メソッドは、キャッシュにアイテムが存在しない場合のみ追加します。実際に追加された場合は true を返し、そうでなければ false を返します。add はアトミック操作です:
Cache::add('key', 'value', $seconds);
#永続的な保存
forever メソッドはアイテムをキャッシュに永続的に保存します。これらのアイテムは期限切れにならないため、forget メソッドで手動で削除する必要があります:
Cache::forever('key', 'value');
Memcached ドライバーを使っている場合、"forever" 保存されたアイテムもキャッシュサイズの上限に達すると削除されることがあります。
#キャッシュからのアイテム削除
forget メソッドを使ってキャッシュからアイテムを削除できます:
Cache::forget('key');
有効期限を0秒または負の値に設定してアイテムを削除することもできます:
Cache::put('key', 'value', 0);
Cache::put('key', 'value', -5);
flush メソッドを使うとキャッシュ全体をクリアできます:
Cache::flush();
キャッシュをフラッシュすると設定されたキャッシュの「プレフィックス」は無視され、キャッシュ内のすべてのエントリが削除されます。他のアプリケーションと共有しているキャッシュをクリアする場合は注意してください。
#Cache ヘルパー
Cache ファサードの代わりに、グローバルな cache 関数を使ってキャッシュからの取得や保存ができます。cache 関数に文字列のキーを1つ渡すと、そのキーの値を返します:
$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 メソッドを、ファサードをテストする ときと同様に使用できます。
#アトミックロック
この機能を利用するには、アプリケーションのデフォルトキャッシュドライバーが memcached、redis、dynamodb、database、file、または array のいずれかである必要があります。また、すべてのサーバーが同じ中央キャッシュサーバーと通信している必要があります。
#ドライバーの前提条件
#データベース
database キャッシュドライバーを使う場合、アプリケーションのキャッシュロックを格納するテーブルを用意する必要があります。以下にテーブルの Schema 宣言例を示します:
Schema::create('cache_locks', function (Blueprint $table) {
$table->string('key')->primary();
$table->string('owner');
$table->integer('expiration');
});
cache:table Artisanコマンドでデータベースドライバーのキャッシュテーブルを作成した場合、そのコマンドで作成されるマイグレーションにはすでにcache_locksテーブルの定義が含まれています。
#ロックの管理
Atomicロックは、競合状態を気にせずに分散ロックを操作できます。例えば、Laravel ForgeはAtomicロックを使い、サーバー上で同時に一つのリモートタスクだけが実行されるようにしています。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秒待ってロックを取得...
});
#プロセス間でのロック管理
場合によっては、あるプロセスでロックを取得し、別のプロセスで解放したいことがあります。例えば、ウェブリクエスト中にロックを取得し、そのリクエストでキューに登録されたジョブの終了時にロックを解放する場合です。この場合、ロックのスコープ付き「オーナートークン」をジョブに渡し、そのトークンでロックを再生成します。
以下の例では、ロック取得に成功したらキュージョブをディスパッチし、ロックのオーナートークンを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メソッド内でキャッシュを読み込む可能性があるため、bootingコールバック内で登録します。bootingコールバックは、全サービスプロバイダーのregisterメソッド実行後、bootメソッド実行直前に呼ばれます。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
{
/**
* アプリケーションサービスの登録
*/
public function register(): void
{
$this->app->booting(function () {
Cache::extend('mongo', function (Application $app) {
return Cache::repository(new MongoStore);
});
});
}
/**
* アプリケーションサービスの起動処理
*/
public function boot(): void
{
// ...
}
}
extend メソッドに渡される最初の引数はドライバー名です。これは config/cache.php 設定ファイルの driver オプションに対応します。2番目の引数は 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;
/**
* アプリケーションのイベントリスナーのマッピング
*
* @var array
*/
protected $listen = [
CacheHit::class => [
LogCacheHit::class,
],
CacheMissed::class => [
LogCacheMissed::class,
],
KeyForgotten::class => [
LogKeyForgotten::class,
],
KeyWritten::class => [
LogKeyWritten::class,
],
];