Cache-asideはキャッシュロジックをアプリケーションに配置します。Read-throughとwrite-throughはそれをキャッシュレイヤー自体に移し、アプリケーションはデータベースであるかのようにキャッシュと通信し、キャッシュがバックエンドストアからのロードと書き込みを処理します。この違いは微妙に聞こえますが、誰がロジックを所有するか、キャッシュの整合性がどのように保たれるか、複雑性がどこに存在するかを変えます。3つのパターンをすべて知ることで、各ユースケースに適した整合性と複雑性のトレードオフを選択できます。

これはcache-asideに続くRedis Masterclassのパート10です。

Read-through: ミス時にキャッシュがロード

read-throughでは、アプリケーションは常にキャッシュから読み取り、キャッシュはミス時にデータベースからのフェッチを担当します。cache-asideではアプリケーションが「DBからロードしてポピュレート」ステップを実行しますが、read-throughはそれをキャッシュレイヤーの背後に隠します:

// アプリケーションコードはシンプル:キャッシュから読み取るだけ
const user = await cache.get(`user:${id}`);
// キャッシュレイヤーは、ミス時にDBからロードし、保存して返す

Enter fullscreen mode Exit fullscreen mode

外部からはcache-asideとほぼ同じ動作:レイジーローディング、TTL、ミス時にキャッシュをポピュレートします。違いはロジックが存在する場所です。read-throughにはデータベースを呼び出せるキャッシュ(プロバイダー、ライブラリ、または自作のラッパー)が必要で、ロードロジックは各呼び出しサイトで繰り返されるのではなく、1か所に集中化されます。

利点は実装の一貫性:すべての読み取りが同じロードパスを通るため、あるコードパスでキャッシュのポピュレートを忘れることがありません。コストは、そのようなloader付きキャッシュレイヤーが必要になることです。実際、多くのチームはcache-asideを単一の共有関数でラップすることでread-throughの利点を得ています。これは特別なインフラなしで同じアイデアを実現する方法です。

Write-through: キャッシュがデータベースに書き込む

write-throughは書き込み側でread-throughとペアになります。アプリケーションはキャッシュに書き込み、キャッシュは確認応答前に同期的にデータベースに書き込みます:

// アプリケーションはキャッシュに書き込む;キャッシュはDBにwrite-throughする
await cache.set(`user:${id}`, updatedUser);
// 内部処理:キャッシュはRedisを更新し、データベースに書き込んでから返す

Enter fullscreen mode Exit fullscreen mode

キャッシュとデータベースの両方が1つの操作で更新されるため、キャッシュは常にデータベースと整合します(無効化の忘れによる古いエントリは発生しません)。すべての書き込みが両者をロックステップで保ちます。

トレードオフは書き込みレイテンシです。書き込みはキャッシュとデータベースの両方に同期的に行くため、書き込みはデータベース書き込み+キャッシュ書き込みと同じだけ遅くなります。書き込みパスにキャッシュを追加したため、書き込みは高速化されません(読み取りは高速化されます)。また、読み取られるかどうかにかかわらず書き込み時にデータをキャッシュするため、コールドデータでキャッシュが埋まる可能性があります。

Read-throughとwrite-throughの組み合わせ:整合性はあるが書き込みは高速化されない

read-throughとwrite-throughを併用すると、自動的にデータベースと整合するキャッシュが得られます:読み取りはミス時にポピュレートされ、書き込みは両方のストアを更新します。アプリケーションはキャッシュをデータのインターフェースとして扱い、キャッシュは自身を同期させます。これはアプリ全体に無効化ロジックを散在させることなく強いキャッシュ整合性が欲しい場合に魅力的です。

正直な限界:書き込みは高速化されない(両方のストアにヒットする)、キャッシュは読み取られない可能性のあるデータを保持する(すべての書き込みで書き込まれる)、パターンをサポートするキャッシュレイヤーが必要です。整合性が重要でキャッシュロジックを集中化したい読み取り中心のデータで威力を発揮し、cache-asideのwrite-on-deleteで十分なシンプルなキャッシングには過剰です。

パターンの選択

これまで3つのパターンを比較すると:

  • Cache-aside:アプリケーションがキャッシュを管理;ミス時にロード、書き込み時に無効化。最もシンプルで一般的、グレースフルに劣化するが、ロジックがアプリ全体に散在し、一貫性が失われる可能性がある。
  • Read-through:キャッシュがミス時にロードし、読み取りロジックを集中化。動作はcache-asideと同じだが、それをサポートするキャッシュレイヤーがある場合はよりクリーン。
  • Write-through:キャッシュが同期的にデータベースに書き込み、書き込みレイテンシと未読データのキャッシングを犠牲にしてキャッシュとデータベースを整合させる。

一般的で実用的なセットアップは、DB更新とキャッシュ無効化を1つのよくテストされた共有関数で実行することでwrite-throughスタイルの整合性を実現しつつ、cache-aside読み取りを使用する方法です。これにより特別なキャッシュプロバイダーなしでほとんどの利点を得られます。

整合性の由来

これらのパターンに共通する本質的なテーマは、キャッシュ整合性がどこで強制されるかです。Cache-asideはアプリケーションが無効化を記憶することに依存します。Write-throughは構造的にそれを強制します。なぜならすべての書き込みが両方のストアを更新するからです。無効化の忘れが古いキャッシュのバグを引き起こしている場合、write-throughスタイルのパターン(または無効化の集中化)に移行することで、整合性を開発者が記憶すべきものではなく自動的なものにできます。

Read-throughとwrite-throughはアプリケーションの制御をキャッシュの自動整合性と交換します。集中化され忘れにくいキャッシュロジックが欲しく、write-throughのレイテンシを許容できる場合に適切な選択です。シンプルさとグレースフルな劣化を求める場合は、cache-asideが依然として主力です。次のパターンはさらに進みます:write-behindは整合性を書き込み速度と交換し、refresh-aheadはミスを完全に避けるために作業を先取りします。

主要なポイント

  • Read-throughはload-on-missロジックをキャッシュレイヤーに移動し、集中化することでどのコードパスでもキャッシュのポピュレートを忘れなくします。
  • Write-throughはキャッシュが同期的にデータベースに書き込むことで、キャッシュとデータベースを自動的に整合させます。
  • Write-throughは書き込みを高速化しません(両方のストアにヒットする)し、読み取られるかどうかにかかわらず書き込み時にデータをキャッシュします。
  • Read-throughとwrite-throughを組み合わせると、書き込みレイテンシとサポートするキャッシュレイヤーを犠牲にして自動的なキャッシュ整合性が得られます。
  • 実用的な代替案は、cache-aside読み取りに加えてDB更新と無効化を1つの関数で実行する方法で、特別なインフラなしで整合性を実現します。

よくある質問

cache-asideとread-throughの違いは何ですか?

cache-asideでは、アプリケーションがミス時にデータベースからロードしてキャッシュをポピュレートします。read-throughでは、キャッシュレイヤー自体がそのロードを実行するため、アプリケーションはキャッシュから読み取るだけです。動作は似ていますが、read-throughはロジックを集中化します。

write-throughキャッシングとは何ですか?

アプリケーションがキャッシュに書き込み、キャッシュが返却前に同期的にデータベースに書き込むパターンです。両方のストアが一緒に更新されるため、書き込みレイテンシが高くなる代償として、キャッシュはデータベースと整合します。

write-throughは書き込みを高速化しますか?

いいえ。書き込みはキャッシュとデータベースの両方に同期的に行くため、少なくともデータベース書き込みと同じだけ遅くなります。Write-throughは読み取り整合性を向上させ、書き込み速度を向上させません;キャッシュが常にポピュレートされ最新であるため、読み取りが恩恵を受けます。

cache-asideよりもread-throughとwrite-throughを使うべきなのはいつですか?

キャッシュ整合性を自動的に強制し、キャッシュロジックをアプリ全体に散在させるのではなく集中化したい場合、そして追加の書き込みレイテンシを許容できる場合です。シンプルなキャッシングとグレースフルな劣化のためには、通常cache-asideで十分です。

write-throughなしでキャッシュの整合性を保つには?

cache-asideを使用しつつ、データベース更新とキャッシュ無効化を1つの共有関数で一緒に実行するように書き込みを集中化します。これにより無効化を忘れにくくし、特別なキャッシュレイヤーなしでwrite-throughの整合性に近づけます。

参考文献


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

著者について

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