これまでのキャッシュパターンでは、キャッシュミスが起きることを前提としています。キーが期限切れになり、リクエストがミスし、誰かがデータベースから再読み込みするコストを負担します。リフレッシュアヘッドはこれを直接的に解決します。ホットキーが期限切れになるのを待ってから再読み込みする(ユーザーが待つ間に)代わりに、期限切れ前にバックグラウンドでキーを更新し、重要なデータに対してキャッシュを常に温かい状態に保ちます。これはより高度なパターンで、予測可能な頻繁にアクセスされるデータで、ユーザーにキャッシュミスを絶対に起こしたくない場合に価値があります。これはRedisマスタークラスのパート12で、ライトビハインドに続きます。

expire-then-reloadの問題

キャッシュアサイドでは、キャッシュされたキーはTTLまで存在し、その後期限切れになります。次のリクエストはミスとなり、データベースから同期的に再読み込みする必要があるため、その不幸なリクエストは遅くなります。ほとんどのデータではこれで問題ありません。時折の遅いリクエストは合理的な代償です。しかし、非常にホットなキー(ホームページの注目コンテンツ、人気の商品、すべてのリクエストが読み込む設定)では、その定期的な遅い再読み込みが絶えず発生し、そのたびに実際のユーザーがデータベースを待つことになります。さらに悪いことに、多くのリクエストが同時に期限切れのキーにヒットすると、それらはすべて一緒に再読み込みし、スタンピードが発生します。

リフレッシュアヘッドの洞察:再びリクエストされることがわかっているデータについては、期限切れを待たないことです。まだ有効な間にプロアクティブに更新し、リクエストは常に温かいキャッシュにヒットするようにします。

リフレッシュアヘッドの仕組み

このパターンは、TTLが切れる前にバックグラウンドでキーを更新し、提供される値がユーザーの下で期限切れになるほど古くならないようにします。実装方法はいくつかあります。

一般的なアプリケーションレベルのアプローチ:キャッシュされた値を配信する際に、期限切れまでの残り時間を確認し、リフレッシュ閾値内であれば、現在の(まだ有効な)値を返しながら非同期リフレッシュをトリガーします:

async function getWithRefreshAhead(key, ttl, refreshThreshold) {
  const remaining = await redis.ttl(key);
  const value = await redis.get(key);

  if (value && remaining < refreshThreshold) {
    // still valid, but expiring soon: refresh in the background, don't wait
    queueRefresh(key, ttl);
  }
  if (value) return JSON.parse(value);      // return current value immediately

  return await loadAndCache(key, ttl);       // cold miss fallback
}

Enter fullscreen mode Exit fullscreen mode

ユーザーは常に温かいキャッシュから即座に応答を得られ、リフレッシュは帯域外で発生します。キーの値は期限切れになる前に更新されるため、ホットキーでは真のミス(同期再読み込みを伴う)がほとんど発生しません。

もう一つのアプローチはスケジュール方式です。バックグラウンドジョブがタイマーで既知のホットキーのセットを定期的に更新し、リクエストとは独立しています。これは、トラフィックに関係なく温かい状態を保ちたい固定の重要なキーのセット(サイト設定、注目リスト)に適しています。

コスト

リフレッシュアヘッドは無料ではなく、そのコストがいつ価値があるかを説明します。再度リクエストされない可能性のあるキーを更新し、投機的に作業を行うため、誰も必要としないデータにデータベースクエリを費やすことになります。また、より複雑です。バックグラウンドリフレッシュメカニズム、閾値ロジック、同じキーを一度に複数回更新しないための注意が必要です。

この投機的作業のコストが、リフレッシュアヘッドがデフォルトではなく対象を絞ったものである理由です。すべてのキーに適用すると、コールドデータのプロアクティブなリフレッシュでデータベースが溢れてしまいます。キーが本当に再度リクエストされることがほぼ確実で、リフレッシュ作業がほぼ常に有用な、真正にホットで予測可能なキーの場合にのみ効果を発揮します。

いつ使用するか

リフレッシュアヘッドは狭く価値のあるケースに適合します:

  • 絶えずアクセスされるホットキー:ほとんどのリクエストのクリティカルパスにあるコンテンツ、設定、データで、ミスが多くのユーザーに影響を与える場合。
  • 予測可能なアクセス:間もなく再度リクエストされることが確実に言えるデータで、プロアクティブなリフレッシュが無駄にならない場合。
  • 計算コストの高い値:再読み込みが高コスト(重いクエリや計算)である場合、同期ミスを回避することは特に価値があります。

ほとんどアクセスされないキーのロングテールには過剰です。キャッシュアサイドのロードオンミスの方が全体的に安価で、それらのキーのほとんどは期限切れ前に再度リクエストされることがないからです。

他のパターンとの組み合わせ

リフレッシュアヘッドはキャッシュアサイドの代替ではなく、その上に重ねる対象を絞った強化です。典型的なセットアップでは、キャッシュの大部分にキャッシュアサイドを使用し、特定された少数のホットキーにリフレッシュアヘッドを追加します。どこでもキャッシュアサイドのシンプルさと優雅な劣化を得られ、最も重要な場所でリフレッシュアヘッドのミス回避を得られます。また、スタンピード問題にも直接役立ちます。ホットキーが期限切れ前にプロアクティブに更新されることで、負荷の下で期限切れになることがなく、同時ミスの群れが形成されることがありません。

リフレッシュアヘッドは、特定のホットデータを永続的に温かい状態に保つためのパターンです。期限切れ前にバックグラウンドでキーを更新し、ユーザーが再読み込みを待つことがないようにします。投機的な作業を行うことを受け入れ、その作業が効果を発揮する予測可能で高トラフィックのキーにのみ適用します。これらのキーでは、定期的な遅い再読み込みを一貫して高速な応答に変えます。

これでコアキャッシングパターンをカバーしました。次に、すべてのパターンが共有する問題に取り組みます。キャッシュ無効化、キャッシングの真に難しい部分、そしてキャッシュデータを正しく保つための戦略です。

主要なポイント

  • リフレッシュアヘッドは、ホットキーを期限切れ前にバックグラウンドで更新し、ユーザーが同期再読み込みのコストを負担しないようにします。
  • 提供されるキーが期限切れまでの閾値内にある場合に更新するか、既知のホットキーに対してリフレッシュをスケジュールすることで実装します。
  • コストは投機的作業です。再度リクエストされない可能性のあるキーを更新するため、予測可能なホットデータに対してのみ効果を発揮します。
  • ほとんどアクセスされないデータのロングテールではなく、少数のホットで予測可能で計算コストの高いキーに適用します。
  • キャッシュアサイドの上にレイヤーされ、ホットキーが負荷の下で期限切れにならないようにすることでスタンピードを防ぎます。

よくある質問

リフレッシュアヘッドキャッシングとは

キャッシュされたキーを期限切れ前にバックグラウンドでプロアクティブに更新するパターンで、頻繁にアクセスされるデータが温かい状態を保ち、リクエストが同期再読み込みにヒットしないようにします。ホットキーのキャッシュミスを回避するために、いくつかの投機的作業をトレードオフします。

リフレッシュアヘッドはキャッシュアサイドとどう違うか

キャッシュアサイドは、キーが期限切れになりリクエストがミスした後にのみキーを再読み込みするため、そのリクエストは遅くなります。リフレッシュアヘッドは、ホットキーを期限切れ前にバックグラウンドで再読み込みするため、ユーザーは常に温かいキャッシュにヒットします。リフレッシュアヘッドは通常、選択したキーに対してキャッシュアサイドの上にレイヤーされます。

リフレッシュアヘッドをいつ使用すべきか

クリティカルパス上にある少数のホットで予測可能なキー(サイト設定、注目コンテンツ、人気のアイテム)に対して、特にそれらの再読み込みが高コストである場合に使用します。ほとんどアクセスされないキーには価値がなく、プロアクティブなリフレッシュ作業が無駄になります。

リフレッシュアヘッドの欠点は

投機的作業を行い、再度リクエストされない可能性のあるキーを更新するため、コールドデータに適用するとデータベースクエリを無駄にします。また、バックグラウンドリフレッシュロジックと冗長なリフレッシュを避けるための注意が必要で、より複雑です。

リフレッシュアヘッドはキャッシュスタンピードに役立つか

はい。ホットキーを期限切れ前に更新することで、キーが負荷の下で期限切れ状態にならず、人気のキーが期限切れになったときに発生する多数の同時ミスによるスタンピードが形成されません。

参考文献


この記事は元々 amanksingh.com/blog/redis-refresh-ahead で公開されました。

著者について

Aman Kumar Singhはインドのノイダを拠点とするチームリード兼シニアソフトウェアエンジニアで、TypeScript、Next.js、NestJS、PostgreSQL、Redisを使用したフルスタックエンジニアリング、システム設計、本番SaaSについて執筆しています。