2026年6月16日

2026年6月16日

RedisのキャッシュパターンとCache-Asideの実装(WordPressでの書き方)

Cache-Aside(キャッシュアサイド)は、アプリケーションがまずキャッシュを見て、無ければデータベースから取得してキャッシュに入れる、という最も広く使われるキャッシュパターンです。この記事では、主なキャッシュパターンの違いを整理したうえで、WordPressのオブジェクトキャッシュAPIを使ったCache-Asideの実装、キャッシュの無効化、アクセス集中時に起きる「キャッシュスタンピード」の防ぎ方までを、動くコードで説明します。

主なキャッシュパターン

キャッシュの使い方には、読み込みと書き込みのそれぞれにいくつかの型があります。名前は文献によって少し揺れますが、考え方は次のとおりです。

  • Cache-Aside(Lazy Loading): アプリケーションがキャッシュを確認し、無ければDBから読んでキャッシュへ書く。更新時はDBを更新してキャッシュを削除する
  • Read-Through: アプリケーションはキャッシュ層にだけ問い合わせ、キャッシュ層が自分でDBから読み込む。キャッシュ製品やライブラリ側の機能が必要
  • Write-Through: 書き込み時にDBとキャッシュの両方へ同時に書く。読み込みは常に新しいが、使われないデータもキャッシュに入る
  • Write-Behind(Write-Back): まずキャッシュに書き、DBへは後でまとめて反映する。速いが、反映前に障害が起きるとデータを失う

WordPressでRedisを使う場面では、ほとんどがCache-Asideです。理由は単純で、Redisは「必要になったデータだけを置く場所」として使い、正しいデータはあくまでMySQL/MariaDBにある、という役割分担が分かりやすいからです。Redisが落ちても、DBから読めばサイトは動き続けます。

Cache-Asideの流れと設計で決めること

Cache-Asideの処理は次の3段階です。

  1. キャッシュをキーで読む。あればそれを返す(キャッシュヒット)
  2. 無ければDBから取得する(キャッシュミス)
  3. 取得した値をキャッシュに書き、有効期限(TTL)を付けて返す

設計で決めるのは主に3つです。1つ目はキー名で、何のデータかが分かり、条件(件数やページ番号など)を含めた一意な名前にします。2つ目はTTLで、データが古くても許される時間を基準に決めます。3つ目は無効化の方法で、データが更新されたときにどのキーを消すか、または自然に期限切れを待つかを決めます。

WordPressでの前提:永続オブジェクトキャッシュ

WordPressには wp_cache_get() や wp_cache_set() といったオブジェクトキャッシュAPIがあります。ただし標準のままでは、キャッシュはそのリクエストの間しか保持されません。リクエストをまたいでRedisに保存するには、wp-content/object-cache.php(ドロップイン)を設置するプラグインが必要です。代表的なものに Redis Object Cache があります。

自分のコードがRedisに保存されているかは、wp_using_ext_object_cache() で判定できます。これが false の環境では、wp_cache_set() で入れた値は次のリクエストで消えているので、トランジェント(set_transient())を使うかどうかを分岐させるとよいでしょう。

実装例:人気記事一覧をCache-Asideでキャッシュする

コメント数の多い記事を一覧表示する処理を例にします。コードはテーマ直下ではなく、子テーマのfunctions.phpか、wp-content/mu-plugins/ に置いてください。導入前にバックアップを取り、ステージングで動作を確認します。

<?php
function myprefix_get_popular_post_ids( $limit = 5 ) {
	$limit        = absint( $limit );
	$group        = 'myprefix';
	// 投稿が更新されるたびに変わる値をキーに含め、古いキャッシュを自然に使わなくする
	$last_changed = wp_cache_get_last_changed( 'posts' );
	$key          = "popular_ids:{$limit}:{$last_changed}";

	// 1. キャッシュを読む(false をキャッシュした場合と区別するため $found を使う)
	$found = false;
	$ids   = wp_cache_get( $key, $group, false, $found );
	if ( $found ) {
		return $ids;
	}

	// 2. 無ければDBから取得する(件数計算を省き、IDだけ取得して軽くする)
	$query = new WP_Query( array(
		'post_type'           => 'post',
		'post_status'         => 'publish',
		'posts_per_page'      => $limit,
		'orderby'             => 'comment_count',
		'order'               => 'DESC',
		'ignore_sticky_posts' => true,
		'no_found_rows'       => true,
		'fields'              => 'ids',
	) );
	$ids = $query->posts;

	// 3. キャッシュに書く。TTLに少し揺らぎを入れて、期限切れが一斉に起きないようにする
	wp_cache_set( $key, $ids, $group, HOUR_IN_SECONDS + wp_rand( 0, 300 ) );

	return $ids;
}

表示側では、取得したIDから出力するときにエスケープします。

<ul class="popular-posts">
<?php foreach ( myprefix_get_popular_post_ids( 5 ) as $post_id ) : ?>
	<li><a href="<?php echo esc_url( get_permalink( $post_id ) ); ?>"><?php echo esc_html( get_the_title( $post_id ) ); ?></a></li>
<?php endforeach; ?>
</ul>

なぜIDだけをキャッシュするのか

投稿オブジェクトをまるごとキャッシュすると、データが大きくなり、タイトル変更などが反映されにくくなります。IDだけを保存しておけば、個々の投稿データはWordPressコアが持つ投稿キャッシュから読まれるため、更新も自然に反映されます。キャッシュは「重い部分(どの記事を並べるかの検索)」に絞るのが効果的です。

キャッシュの無効化

Cache-Asideで一番難しいのは、データが変わったときにキャッシュをどう扱うかです。方法は主に3つあります。

TTLによる自然な期限切れ

最も単純な方法です。「1時間程度なら古くても構わない」データなら、TTLだけで十分です。上の例でTTLに数分の揺らぎを加えているのは、多数のキーが同時に期限切れになり、DBへ一斉にアクセスが向かうのを避けるためです。

last_changed をキーに含める

上の例で使った wp_cache_get_last_changed( 'posts' ) は、投稿のキャッシュが削除されるたびに更新される値を返します。これをキー名に含めると、投稿が更新された時点でキー名が変わり、古いキャッシュは使われなくなります。古いキーはTTLで自然に消えます。どのキーを消すべきか列挙しにくい一覧系のキャッシュに向いた方法です。

更新フックで明示的に削除する

特定のキーが分かっている場合は、更新時に直接消します。

add_action( 'save_post_product', function ( $post_id ) {
	wp_cache_delete( 'product_summary:' . $post_id, 'myprefix' );
} );

順番は「DBを更新してからキャッシュを削除する」が基本です。キャッシュを書き換える(更新後の値をセットする)方法もありますが、同時に複数の更新が走ると古い値で上書きされることがあるため、削除して次の読み込みで作り直すほうが安全です。

キャッシュスタンピードを防ぐ

アクセスの多いページで重いキャッシュが期限切れになると、その瞬間に多数のリクエストが同時にキャッシュミスし、全員がDBへ同じ重い問い合わせを送ります。これがキャッシュスタンピード(ドッグパイル)です。対策としては、「1人だけが作り直し、ほかの人は古い値を使う」形にするのが一般的です。

function myprefix_get_with_lock( $key, $group, $ttl, callable $build ) {
	$found  = false;
	$cached = wp_cache_get( $key, $group, false, $found );

	// 有効期限内なら、そのまま返す
	if ( $found && $cached['expires'] > time() ) {
		return $cached['value'];
	}

	// 作り直す権利を1つのリクエストだけが取る(キーが無いときだけ成功する)
	if ( wp_cache_add( $key . ':lock', 1, $group, 30 ) ) {
		$value = call_user_func( $build );
		// 実際の保存期間は長めにし、データの中に「本来の期限」を持たせる
		wp_cache_set( $key, array( 'value' => $value, 'expires' => time() + $ttl ), $group, $ttl * 2 );
		wp_cache_delete( $key . ':lock', $group );
		return $value;
	}

	// ロックを取れなかった場合は古い値を返し、無ければその場で作る
	return $found ? $cached['value'] : call_user_func( $build );
}

wp_cache_add() は、キーがまだ無いときだけ保存して true を返す関数です。永続オブジェクトキャッシュが有効な環境では、これをリクエスト間の簡易ロックとして使えます。ただし、ロックの厳密さはドロップインの実装に依存するので、厳密な排他制御が必要な処理には向きません。永続キャッシュが無い環境ではリクエストごとに独立するため、ロックとして機能しない点にも注意してください。

Redisを直接使う場合

WordPress以外のPHPアプリケーションでphpredisを直接使う場合も、考え方は同じです。ロックはRedisの SET に NX(キーが無いときだけ)とEX(秒単位の期限)を付けて取ります。

$redis = new Redis();
$redis->connect( '127.0.0.1', 6379, 1.0 );

$value = $redis->get( 'report:daily' );
if ( false === $value ) {
	if ( $redis->set( 'report:daily:lock', '1', array( 'nx', 'ex' => 30 ) ) ) {
		$value = json_encode( build_daily_report() ); // DBから集計する重い処理
		$redis->set( 'report:daily', $value, array( 'ex' => 3600 ) );
		$redis->del( 'report:daily:lock' );
	}
}

キャッシュ用のRedisでは、メモリが上限に達したときに古いキーを追い出す設定(maxmemory-policy を allkeys-lru などにする)もあわせて確認しておくと、キャッシュでメモリがあふれる事態を防げます。既定値や推奨設定はバージョンや提供元で異なる場合があるため、利用中のサーバーで確認してください(2026年10月現在)。

まとめ

  • Cache-Asideは「キャッシュを見る → 無ければDB → キャッシュに書く」の3段階で、正しいデータはDBに置いたまま使える
  • WordPressでリクエストをまたいでRedisに保存するには、object-cache.phpを設置するプラグインが必要。wp_using_ext_object_cache() で判定できる
  • キャッシュするのは重い検索結果(IDの一覧など)に絞り、出力時はエスケープする
  • 無効化はTTL、wp_cache_get_last_changed() を使ったキー、更新フックでの削除を使い分ける
  • 人気ページではTTLの揺らぎとロックでスタンピードを防ぐ

お気軽にご相談ください

お見積りへ お問い合わせへ
▲