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段階です。
- キャッシュをキーで読む。あればそれを返す(キャッシュヒット)
- 無ければDBから取得する(キャッシュミス)
- 取得した値をキャッシュに書き、有効期限(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の揺らぎとロックでスタンピードを防ぐ