Nette Caching

キャッシュは、かつて取り出すのに計算の手間がかかったデータを保存しておき、次からは速く取り出せるようにして、アプリケーションを速くします。ここでは次のことを扱います。

  • キャッシュの使い方
  • 保管の仕組みの変え方
  • キャッシュを正しく無効にする方法

Nette でのキャッシュの使い方はごく分かりやすく、それでいて洗練された必要にも応えます。性能と 100% の堅牢さを目指して作られています。よく使われる保管の仕組みのアダプタが備わっています。タグによる無効化、時間による期限切れ、キャッシュスタンピードへの守りなどに対応しています。

インストール

パッケージは Composerでダウンロードしてインストールします。

composer require nette/caching

基本の使い方

キャッシュを扱う中心の要素は Nette\Caching\Cacheオブジェクトです。そのインスタンスを作り、コンストラクタに保管の仕組みのオブジェクトを渡します。この保管のオブジェクトは、データが実際に置かれる場所(データベース、Memcached、ディスクのファイルなど)を表します。ふつうは dependency injectionNette\Caching\Storage の型を求めて手に入れます。基本は保管の仕組みの節で学べます。

バージョン 3.0 では、このインターフェースにはまだ I の接頭辞が付いていて、名前は Nette\Caching\IStorage でした。さらに Cache クラスの定数は大文字で書かれていて、たとえば Cache::Expire ではなく Cache::EXPIRE でした。

以下の例では、別名 Cache があり、$storage の変数に保管のインスタンスが入っているものとします。

use Nette\Caching\Cache;

$storage = /* ... */; // Nette\Caching\Storage のインスタンス

キャッシュは要するに キーと値の保管場所 で、連想配列のようにキーでデータを読み書きします。アプリケーションは独立した複数の部分から成ります。すべての部分がひとつの保管場所(ディスクのひとつのディレクトリを想像してください)を使えば、遅かれ早かれキーの衝突が起きます。Nette Framework はこれを、保管の空間を名前空間(考え方としては下位のディレクトリのようなもの)に分けることで解決します。アプリケーションのそれぞれの部分は、一意の名前を持つ自分の名前空間の中で働くので、衝突は起きません。

名前空間の名前は Cache クラスのコンストラクタの第 2 引数で指定します。

$cache = new Cache($storage, 'Full Html Pages');

必要なら、既存のインスタンスから derive() メソッドで下位の名前空間に絞った新しいキャッシュを作れます。

$subCache = $cache->derive('Images');

これで $cache のオブジェクトでキャッシュを読み書きできます。load() メソッドが両方の役目を果たします。第 1 引数はキー、第 2 引数はそのキーがキャッシュに見つからないときに呼ばれる PHP のコールバックです。コールバックが値を作って返し、load() メソッドがそれを蓄えます。

$value = $cache->load($key, function () use ($key) {
	$computedValue = /* ... */; // 手間のかかる計算
	return $computedValue;
});

第 2 パラメータを省くと($value = $cache->load($key))、その項目がキャッシュに見つからなければ load()null を返します。

文字列だけでなく、直列化できるどんな構造も蓄えられるのはうれしいところです。キーについても同じです。

キャッシュから項目を消すには remove() メソッドを使います。

$cache->remove($key);

項目は $cache->save($key, $data, ?array $dependencies = null) メソッドでも保存できます。とはいえ、上で見た load() のやり方のほうがふつうは好まれます。

メモ化

メモ化とは、関数やメソッドの呼び出しの結果を蓄えて、次に同じ引数で呼ばれたときに計算し直さず蓄えた結果を返すことです。

メソッドと関数は call(callable $callback, ...$args) でメモ化して呼べます。

$result = $cache->call('gethostbyaddr', $ip);

こうすると gethostbyaddr() の関数は、$ip の引数の値ごとに一度だけ呼ばれます。同じ $ip での次の呼び出しは、蓄えられた値を返します。

メソッドや関数のメモ化された包みを作って、あとで呼ぶこともできます。

function factorial($num)
{
	return /* ... */;
}

$memoizedFactorial = $cache->wrap('factorial');

$result = $memoizedFactorial(5); // 一度めは計算します
$result = $memoizedFactorial(5); // 二度めはキャッシュから返します

期限切れと無効化

キャッシュを使うなら、前に保存したデータがいつ古くなるかという問題に向き合う必要があります。Nette Framework は、データの有効さを制限したり、はっきり消したりするしくみを備えています(フレームワークの言い方では「無効化」と呼びます)。

データの有効さは保存のときに決めます。ふつうは save() メソッドの第 3 パラメータを使います。たとえば次のようにです。

$cache->save($key, $value, [
	$cache::Expire => '20 minutes',
]);

あるいは、load() メソッドのコールバックに参照で渡される $dependencies のパラメータで決められます。たとえば次のようにです。

$value = $cache->load($key, function (&$dependencies) {
	$dependencies[Cache::Expire] = '20 minutes';
	return /* ... */;
});

あるいは load() メソッド自身の第 3 パラメータでも決められます。たとえば次のようにです。

$value = $cache->load($key, function () {
	return /* ... */;
}, [Cache::Expire => '20 minutes']);

以下の例では、コールバックの中で $dependencies の変数を使う 2 つめの形を前提にします。

期限切れ

いちばん単純な期限切れの形は時間の制限です。次はデータを 20 分の有効期間で蓄えます。

// 秒数や UNIX タイムスタンプも受け取ります
$dependencies[Cache::Expire] = '20 minutes';

読むたびに有効期間を延ばしたいなら(スライド式の期限切れ)、次のようにできます。ただしキャッシュの手間が増えることに注意してください。

$dependencies[Cache::Sliding] = true;

特定のファイル、あるいはいくつかのファイルのどれかが変わったときにデータを期限切れにするのも便利な選択肢です。たとえばそれらのファイルを処理して得たデータを蓄えるときに役立ちます。絶対パスを使ってください。

$dependencies[Cache::Files] = '/path/to/data.yaml';
// または
$dependencies[Cache::Files] = ['/path/to/data1.yaml', '/path/to/data2.yaml'];

ほかの特定の項目(あるいはいくつかのうちのどれか)が期限切れになったときに、キャッシュの項目を期限切れにできます。たとえば HTML のページ全体とその断片を違うキーで蓄えるときに役立ちます。断片が変わったら、ページ全体を無効にすべきです。断片が frag1frag2 のキーで蓄えられているなら、次のようにします。

$dependencies[Cache::Items] = ['frag1', 'frag2'];

期限切れは独自の関数や静的メソッドでも決められます。それらは読むたびに呼ばれ、その項目がまだ有効かを判断します。たとえば PHP のバージョンが変わるたびに項目を期限切れにできます。今のバージョンをパラメータと比べる関数を作り、保存のときに [関数名, ...引数] の形の配列を依存関係に足します。

function checkPhpVersion($ver): bool
{
	return $ver === PHP_VERSION_ID;
}

$dependencies[Cache::Callbacks] = [
	['checkPhpVersion', PHP_VERSION_ID] // checkPhpVersion(...) === false のとき期限切れ
];

もちろん、これらの基準はすべて組み合わせられます。少なくともひとつの基準が満たされなくなると、そのキャッシュの項目は期限切れになります。

$dependencies[Cache::Expire] = '20 minutes';
$dependencies[Cache::Files] = '/path/to/data.yaml';

タグによる無効化

タグはとても役に立つ無効化のしくみです。キャッシュに蓄えるそれぞれの項目に、タグ(好きな文字列)の一覧を割り当てられます。たとえば、記事とそのコメントを表示する HTML のページを蓄えたいとしましょう。保存のときに、それに合うタグを指定します。

$dependencies[Cache::Tags] = ["article/$articleId", "comments/$articleId"];

では管理画面へ移りましょう。ここには記事を編集するフォームがあります。記事をデータベースへ保存するのと合わせて、clean() メソッドを呼んでタグをもとに蓄えた項目を消します。

$cache->clean([
	$cache::Tags => ["article/$articleId"],
]);

同じように、新しいコメントを足す(あるいは編集する)ときも、対応するタグを無効にするのを忘れてはいけません。

$cache->clean([
	$cache::Tags => ["comments/$articleId"],
]);

これで何が実現したのでしょうか。私たちの HTML のキャッシュは、それに結び付いた記事やそのコメントが変わるたびに無効化(削除)されるようになりました。ID = 10 の記事を編集すると、タグ article/10 が無効になり、このタグを持つ蓄えられた HTML のページが消されます。その記事の下に新しいコメントが足されたときも同じです。

タグにはジャーナルが要ります。

優先度による無効化

キャッシュの項目ごとに優先度を割り当てられます。おかげで、たとえばキャッシュがある大きさの制限を超えたときに、制御された形で消せます。

$dependencies[Cache::Priority] = 50;

優先度が 100 以下のすべての項目を消すには次のようにします。

$cache->clean([
	$cache::Priority => 100,
]);

優先度にはいわゆるジャーナルが要ります。

キャッシュを空にする

Cache::All のパラメータはすべてを空にします。

$cache->clean([
	$cache::All => true,
]);

まとめて読む

キャッシュをまとめて読み書きするには bulkLoad() メソッドを使います。キーの配列を渡すと、それに対応する値の配列が返ります。

$values = $cache->bulkLoad($keys);

bulkLoad() メソッドは load() と同じように働き、第 2 パラメータのコールバックも受け取ります。このコールバックは、作られる項目のキーを受け取ります。

$values = $cache->bulkLoad($keys, function ($key, &$dependencies) {
	$computedValue = /* ... */; // 手間のかかる計算
	return $computedValue;
});

逆に複数の項目を一度に書くには bulkSave() メソッドを使います。これは キー => 値 の組の配列と、省略できる依存関係を受け取ります。

$cache->bulkSave([
	$key1 => $value1,
	$key2 => $value2,
], [Cache::Expire => '20 minutes']);

PSR-16 との組み合わせ

Nette Cache を PSR-16 のインターフェースで使うには、PsrCacheAdapter を使えます。これで Nette Cache と、PSR-16 に合うキャッシュの実装を期待するどんなコードやライブラリともなめらかに組み合わせられます。

$psrCache = new Nette\Bridges\Psr\PsrCacheAdapter($storage);

これで $psrCache を標準の PSR-16 のキャッシュとして使えます。

$psrCache->set('key', 'value', 3600); // 値を 1 時間蓄えます
$value = $psrCache->get('key', 'default');

このアダプタは、getMultiple()setMultiple()deleteMultiple() も含めて PSR-16 が定めるすべてのメソッドに対応しています。

出力の蓄え

出力はとても優雅に捕まえて蓄えられます。

if ($capture = $cache->capture($key)) {

	// echo ... なんらかのデータを出力します

	$capture->end(); // 出力をキャッシュへ保存します
}

出力がすでにキャッシュにあれば、capture() メソッドはそれを出力して null を返すので、if の中身は飛ばされます。そうでなければ出力の蓄えを始め、$capture のオブジェクトを返します。捕まえたデータは、その end() メソッドで最後にキャッシュへ保存します。

バージョン 3.0 では、このメソッドの名前は $cache->start() でした。

Latte でのキャッシュ

Latteのテンプレートでのキャッシュはとても簡単です。蓄えたいテンプレートの部分を {cache}...{/cache} のタグで包むだけです。もとのテンプレートのファイルが変わると(蓄えられたかたまりの中で取り込まれたテンプレートも含みます)、キャッシュは自動的に無効になります。{cache} のタグは入れ子にできます。入れ子のかたまりが(たとえばタグで)無効になると、その親のかたまりも無効になります。

タグの中では、そのキャッシュの項目を結び付けるキー(ここでは変数 $id)を指定したり、有効期限を決めたり、無効化のタグを定めたりできます。

{cache $id, expire: '20 minutes', tags: [tag1, tag2]}
	...
{/cache}

これらのパラメータはすべて省略できるので、有効期限もタグもキーも指定しなくてかまいません。

キャッシュを使うかどうかを if で条件にもできます。条件が満たされたときにだけ中身が蓄えられます。

{cache $id, if: !$form->isSubmitted()}
	{$form}
{/cache}

保管の仕組み

保管の仕組みは、データが実際に置かれる場所を表すオブジェクトです。データベース、Memcached のサーバー、あるいはもっとも手近な保管場所であるディスクのファイルを使えます。

保管の仕組み 説明
FileStorage 既定の保管の仕組み。キャッシュをディスクのファイルに保存します。
MemcachedStorage 保管に Memcached のサーバーを使います。
MemoryStorage データを一時的にメモリに置きます(リクエストの終わりに失われます)。
SQLiteStorage データを SQLite のデータベースのファイルに置きます。
DevNullStorage データは実際には保存されません。テストに役立ちます。

保管のオブジェクトは dependency injectionNette\Caching\Storage の型を求めて手に入れます。既定では、Nette は一時ファイルのディレクトリの中の cache の下位のディレクトリにデータを置く FileStorage オブジェクトを与えます。

既定の保管の仕組みは設定で変えられます。

services:
	cache.storage: Nette\Caching\Storages\DevNullStorage

FileStorage

キャッシュの項目をディスクのファイルに書きます。Nette\Caching\Storages\FileStorage の保管の仕組みは性能のために念入りに最適化されていて、そして何より操作の完全なアトミック性を保証します。それはどういうことでしょうか。このキャッシュを使っているとき、ほかのスレッドがまだ完全に書き終えていないファイルを読んでしまうことも、読んでいる最中に誰かがそれを消してしまうことも起こりえません。ですからこの保管の仕組みを使うのは完全に安全です。

この保管の仕組みには、キャッシュが消されたときや、まだ「冷たい」(つまりまだ作られていない)ときに CPU の使用が跳ね上がるのを防ぐ、大事な組み込みの機能もあります。これは キャッシュスタンピード への守りとして知られています。これは、同時に走る複数のリクエストが同じキャッシュの項目(たとえば手間のかかる SQL のクエリの結果)を同時に求めたときに起こります。その項目がそのときキャッシュになければ、これらのプロセスがすべて同じ手間のかかる処理(SQL のクエリなど)を始めてしまうかもしれません。これはサーバーの負荷を何倍にもし、どのスレッドも制限時間の中で応えられず、キャッシュも作られず、アプリケーションが落ちることさえあります。幸い Nette のキャッシュはこれに対処します。同じ項目への同時のリクエストが複数あるとき、それを作るのは最初のスレッドだけです。ほかのスレッドは待って、そのあと最初のスレッドが作った結果を使います。

FileStorage を作る例です。

// 保管場所はディスクの '/path/to/temp' のディレクトリになります
$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp');

MemcachedStorage

Memcachedのサーバーは、高い性能を持つ分散型のメモリのオブジェクトのキャッシュのしくみです。Nette でのそのアダプタは Nette\Caching\Storages\MemcachedStorage です。設定では、サーバーの IP アドレスと、標準の 11211 と違うならポートを指定します。

PHP の memcached 拡張が要ります。

services:
	cache.storage: Nette\Caching\Storages\MemcachedStorage('10.0.0.5')

MemoryStorage

Nette\Caching\Storages\MemoryStorage は、データを PHP の配列の中に持つ保管の仕組みです。ですからリクエストが終わるとデータは失われます。

SQLiteStorage

SQLite のデータベースと Nette\Caching\Storages\SQLiteStorage のアダプタは、ディスクのひとつのファイルの中にデータを蓄える方法を与えます。設定ではそのデータベースのファイルへのパスを指定します。

PHP の pdopdo_sqlite の拡張が要ります。

services:
	cache.storage: Nette\Caching\Storages\SQLiteStorage('%tempDir%/cache.db')

DevNullStorage

特別な保管の実装が Nette\Caching\Storages\DevNullStorage で、これは実際にはデータを何も保存しません。ですからキャッシュの影響をなくしたいテストの用途に向いています。

コードでキャッシュを使う

コードでキャッシュを使うやり方は主に 2 つあります。ひとつめは、dependency injectionで保管のオブジェクトを手に入れて、Cache のオブジェクトを自分で作ることです。

use Nette;

class ClassOne
{
	private Nette\Caching\Cache $cache;

	public function __construct(Nette\Caching\Storage $storage)
	{
		$this->cache = new Nette\Caching\Cache($storage, 'my-namespace');
	}
}

もうひとつは、Cache のオブジェクトを直接求めることです。

class ClassTwo
{
	public function __construct(
		private Nette\Caching\Cache $cache,
	) {
	}
}

その場合、Cache のオブジェクトは設定で定めなければなりません。たとえば次のようにです。

services:
	- ClassTwo( Nette\Caching\Cache(namespace: 'my-namespace') )

ジャーナル

Nette はタグと優先度の情報を、いわゆるジャーナルに保存します。既定では journal.s3db のファイルを通して SQLite が使われ、PHP の pdopdo_sqlite の拡張が要ります

ジャーナルの実装は設定で変えられます。

services:
	cache.journal: MyJournal

DI のサービス

DI コンテナには次のサービスが足されます。

名前 説明
cache.journal Nette\Caching\Storages\Journal キャッシュのジャーナルの保管
cache.storage Nette\Caching\Storage 主なキャッシュの保管

キャッシュを切る

アプリケーションでキャッシュを切るひとつの方法は、保管の仕組みを DevNullStorageにすることです。

services:
	cache.storage: Nette\Caching\Storages\DevNullStorage

この設定は、Latte のテンプレートや DI コンテナのキャッシュには影響しません。これらのライブラリは nette/caching のサービスを使わず、自分でキャッシュを管理するからです。しかも開発モードでは、それらのキャッシュをふつう切る必要はありません

新しい版へ上げるなら、アップグレードのページをご覧ください。

バージョン: 3.x