L1・L2 二段キャッシュの設計
データベースが耐えられなくなったとき、誰もが最初に思いつくのは Redis の導入です。その Redis も苦しくなってきたら、いよいよ多段キャッシュの出番です。この記事では、最もよく使われる L1 + L2 キャッシュの設計について語ります。
なぜ多段キャッシュが必要なのか
インターネットシステムでは、ビジネス規模の拡大に伴い、データベースがシステムの性能ボトルネックになりがちです。大量のリクエストが直接データベースにアクセスすると、高いレイテンシを招くだけでなく、データベースへの負荷が過大になり、コネクションの枯渇やクエリの遅延といった問題まで発生します。そのため、ほぼすべての高並行システムは キャッシュ(Cache) を導入してシステム性能を高めています。
最も一般的なキャッシュ方式は、Redis を統一キャッシュ層として使うことです。クライアントのリクエストはまず Redis にアクセスし、キャッシュにヒットすればそのままデータを返します。ヒットしなければデータベースにアクセスし、結果をキャッシュに書き込みます。この方式だけでもデータベースの負荷を大幅に下げられます。
しかしシステム規模がさらに拡大すると、Redis だけに頼っていては新たな問題が出てきます。例えば高 QPS の場面では、大量のリクエストが同時に Redis にアクセスすることで、ネットワークのオーバーヘッドと Redis の CPU 負荷が発生します。また、特定のホットデータが頻繁に読まれる場合、毎回ネットワーク越しに Redis にアクセスするため、余分なレイテンシも生じます。
これらの問題を解決するために、多くの大規模システムは 多段キャッシュアーキテクチャ(Multi-Level Cache) を導入しています。その中で最も一般的なパターンが L1 キャッシュ + L2 キャッシュの設計 です。
L1・L2 キャッシュの基本アーキテクチャ

L1・L2 キャッシュは本質的に キャッシュの階層アーキテクチャ であり、通常システムには二層のキャッシュを設計します。
L1 キャッシュ(一次キャッシュ)
L1 キャッシュは通常 アプリケーションプロセスの内部 に置かれ、ローカルキャッシュ(Local Cache) とも呼ばれます。例えば:
- Go:bigcache / ristretto
- Java:Caffeine / Guava Cache
- Node:LRU Cache
L1 キャッシュの特徴は次の通りです。
- アクセスが極めて高速(メモリアクセス)
- ネットワーク通信が不要
- 各サービスインスタンスが自分専用のキャッシュを持つ
L2 キャッシュ(二次キャッシュ)
L2 キャッシュは通常 分散キャッシュシステム で、最も一般的なのは Redis や Memcached です。すべてのサービスインスタンスが同一のキャッシュシステムを共有します。
L2 キャッシュの特徴は次の通りです。
- 複数ノードで共有できる
- データの整合性がより良い
- 容量がより大きい
- 分散デプロイをサポートする
システム全体のアクセスの流れは通常、クライアントのリクエスト → サービスプロセス → L1 キャッシュを照会 → ミスなら L2 キャッシュを照会 → それでもミスならデータベースを照会、となります。
L1・L2 キャッシュのアクセスフロー
実際のシステムにおける一回のリクエストの典型的なアクセスフローは次の通りです。
まず、リクエストがアプリケーションサービスに入ると、システムは優先的に L1 キャッシュ を照会します。データが存在すればそのまま結果を返します。このケースはレイテンシが最も低く、通常はマイクロ秒レベルで済みます。
L1 キャッシュがミスした場合は、続けて L2 キャッシュ(Redis) を照会します。Redis にデータが存在すれば結果を返すとともに、そのデータを L1 キャッシュに書き込み、以降のリクエストがローカルキャッシュに直接ヒットできるようにします。
Redis にもデータがなければキャッシュミスです。このときシステムはデータベースにアクセスする必要があります。データベースが結果を返した後、システムは Redis と L1 キャッシュの両方に書き込み、新しいキャッシュデータを構築します。
フロー全体はこう理解できます。
L1 Cache → L2 Cache → Database
この方式により、大部分のリクエストはキャッシュ層で受け止められ、データベースへの頻繁なアクセスを回避できます。
なぜ二段のキャッシュを設計するのか
L1 キャッシュと L2 キャッシュはそれぞれ異なる問題を解決しており、両者を組み合わせることでシステム性能を大幅に向上させられます。
Redis の負荷を下げる
高並行システムでは、すべてのリクエストが Redis にアクセスすると、Redis 自体が新たなボトルネックになる可能性があります。L1 キャッシュによって、大量のホットデータをアプリケーションのメモリ内で直接返せるため、Redis へのリクエスト数を減らせます。
ネットワークオーバーヘッドを減らす
Redis へのアクセスにはネットワーク通信が必要です。レイテンシがわずか 1ms だとしても、高 QPS の場面では無視できないオーバーヘッドになります。一方 L1 キャッシュはプロセス内アクセスであり、ネットワーク呼び出しよりはるかに高速です。
システムのスループットを高める
L1 キャッシュによって、Redis やデータベースの負荷を増やすことなく、システムはより多くのリクエストを処理できます。
ホットデータへのアクセス効率を高める
多くのビジネスシーンでは、少数のホットデータが頻繁にアクセスされます。例えば人気商品、ユーザー情報、設定データ、レコメンド結果などです。L1 キャッシュを使えば、これらのホットデータをローカルで直接ヒットさせられます。
L1 キャッシュ設計で注意すべき問題
L1 キャッシュの性能は優れていますが、新たな問題ももたらします。
1) データの整合性
L1 キャッシュは各サービスインスタンス内に存在するため、インスタンスごとのキャッシュデータが同期していない可能性があります。データが更新されたとき、適切なキャッシュ無効化の仕組みがなければ、古いデータを読んでしまうかもしれません。この問題を解決するために、通常は キャッシュ無効化戦略 を採用します。例えばデータ更新時に、メッセージキューや Pub/Sub の仕組みで全サービスインスタンスに通知し、ローカルキャッシュをクリアさせます。
2) キャッシュ容量の制御
L1 キャッシュはアプリケーションのメモリ内に存在するため、キャッシュデータが多すぎると大量のメモリを占有し、システムの安定性に影響します。そのため通常は LRU や LFU などの戦略でキャッシュサイズを制限します。
3) キャッシュペネトレーションとキャッシュブレイクダウン
大量のリクエストが存在しないデータに同時にアクセスすると、データベースを直撃する可能性があります。通常はブルームフィルターやミューテックスロックの仕組みで防御します。
キャッシュ更新戦略の設計
多段キャッシュシステムでは、キャッシュ更新戦略が非常に重要です。代表的な戦略には次のものがあります。
1) Cache Aside(キャッシュアサイド)
アプリケーションがまずキャッシュを照会し、ミスならデータベースを照会してからキャッシュに書き込みます。最も一般的なパターンです。
2) Write Through(ライトスルー)
アプリケーションがデータベースに書き込むと同時にキャッシュも更新し、キャッシュとデータベースのデータの同期を保証します。
3) Write Back(ライトバック)
アプリケーションはキャッシュにのみ書き込み、キャッシュシステムが非同期でデータベースに書き込みます。この方式は性能に優れますが、実装は複雑です。
インターネットシステムでは、実装がシンプルで安定していることから、大多数のビジネスが Cache Aside パターン を採用しています。
L1・L2 キャッシュアーキテクチャの適用シーン
多段キャッシュアーキテクチャは通常、次のような場面に適しています。
- 読み取りが高並行なシステム:商品詳細ページ、ユーザー情報照会、設定の読み取りなど。
- ホットデータへのアクセスが頻繁:ランキング、レコメンドリストなど。
- データベースの負荷が大きい:キャッシュ層によってデータベースへのクエリ回数を大幅に減らせます。
金融取引システムのように、リアルタイムの整合性要求が非常に高いシステムでは、通常は多段キャッシュを使わず、データベースに直接アクセスします。
まとめ
L1・L2 キャッシュの核心となる考え方は、階層で受け止めることです。ローカルキャッシュが最もホットなトラフィックを飲み込み、Redis がインスタンス間で共有されるデータを引き受け、データベースは本当のキャッシュミスだけを処理します。このアーキテクチャはアクセスレイテンシとバックエンドの負荷を大幅に下げられますが、その代償として複数レプリカの整合性問題を持ち込むため、無効化通知、容量制御、ペネトレーション対策とセットで設計する必要があります。多段キャッシュを導入する価値があるかどうかは、システムの読み書き比率と整合性要求次第です。整合性に敏感な場面では、シンプルな方式のほうがかえって安全です。
COMMENTS