MENU

キャッシュ設計パターン入門|Cache-Aside・Write-Through・Write-BackをAWSとAzureで実装するアーキテクチャ判断ガイド

「キャッシュを入れたら一度読んだデータが古いまま返ってくる」「書き込み後にキャッシュとDBが食い違って障害になった」——クラウドにアーキテクチャを移してから、こういったトラブルに遭遇したことはないだろうか。

オンプレミスでは物理的にAPサーバーとDBが近く、Read性能も十分だったため、キャッシュはあくまで”あると嬉しい”オプションだった。しかしクラウドでは、RDSのコネクション上限・マイクロサービス間通信のレイテンシ・高トラフィック時のDB過負荷といった構造的な問題が表面化し、キャッシュは設計の必須要素になった。

この記事では、クラウドアーキテクチャで使われる3大キャッシュパターン(Cache-Aside・Write-Through・Write-Back)の違いと選定基準を整理する。AWSのElastiCacheとMemoryDB、AzureのAzure Cache for Redisを使った実装判断の基準も合わせて解説する。

キャッシュ設計パターン入門|Cache-Aside・Write-Through・Write-BackをAWSとAzureで実装するアーキテクチャ判断ガイド - 解説

目次

なぜクラウド時代にキャッシュ設計が複雑になったのか

オンプレミスのシステムでキャッシュが「あれば嬉しい」程度の存在だった理由はシンプルだ。物理サーバー同士はデータセンター内で数十マイクロ秒以内のレイテンシで通信でき、DBのコネクション上限も設定で緩和できた。キャッシュを入れなくてもスペックを上げれば何とかなることが多かった。

クラウドでは話が変わる。

・RDSのコネクション上限問題: Amazon RDSのmax_connectionsはインスタンスサイズに依存する。db.t3.microでは約60接続しか張れない。Lambda関数が並列実行されると、あっという間に上限に達してエラーになる。
・マイクロサービス間レイテンシの積算: APIが5つのサービスを呼ぶ構成では、各サービスのDBクエリのレイテンシが積算される。重要な参照データをキャッシュするだけで体感レスポンスが大きく変わる。
・課金単位の問題: クラウドDBはストレージI/Oやクエリ数で課金が発生する。参照頻度の高いデータをキャッシュするだけでDBの課金を数十%削減できるケースも珍しくない。

一方で、クラウドのManaged Redisサービス(ElastiCache・MemoryDB・Azure Cache for Redis)はオンプレでRedisクラスターを構築・運用するコストに比べて圧倒的に安く手軽になった。「キャッシュを本番で使うのはリスクが高い」という心理的ハードルが下がったのはいいことだが、キャッシュパターンを理解しないままキャッシュを入れると一貫性の問題が発生する。

3大キャッシュパターンの概要と選定の軸

キャッシュパターンには大きく「読み込み戦略」と「書き込み戦略」の2軸がある。

読み込み戦略で最も普及しているのがCache-Aside(Lazy Loading)だ。キャッシュにデータが無いときだけDBを読む。

書き込み戦略には主にWrite-Through(キャッシュとDBに同時書き込み)とWrite-Back(キャッシュに書いてから後でDBに反映)の2つがある。

パターン 分類 読み込み 書き込み 一貫性 主なリスク
Cache-Aside 読み込み戦略 Cache Miss→DBを読む DBに書く→キャッシュを削除または更新 TTL依存で弱め コールドスタート・スタンピード
Write-Through 書き込み戦略 Cache Hitを期待 キャッシュとDBを同時更新 高い 書き込みレイテンシ増加
Write-Back 書き込み戦略 Cache Hitを期待 キャッシュのみ→後でDB反映 低い(データ損失リスクあり) キャッシュ障害時のデータ消失

実際の設計では「Cache-Aside + Write-Through」を組み合わせて読み書き双方を制御するケースが多い。Write-Backは書き込みが非常に多い場合の最終手段として検討する。

Cache-Aside(キャッシュ外側配置)パターン

Cache-Asideは「アプリケーションがキャッシュを直接制御する」モデルだ。サービスが最初にキャッシュを確認し、データがなければ(Cache Miss)DBから取得してキャッシュに書く。Lazy Loadingとも呼ばれ、実際に使われたデータだけがキャッシュに乗る点が特徴だ。

1. 読み込みフロー

処理順序は次の通りだ。

# アプリケーション側のキャッシュ制御(Python 疑似コード) def get_product(product_id): # 1. キャッシュを確認 cached = redis.get(f"product:{product_id}") if cached: return json.loads(cached) # Cache Hit → 即返却 # 2. Cache Miss → DBから取得 product = db.query("SELECT * FROM products WHERE id = %s", product_id) # 3. キャッシュに書いてから返す(TTL=3600秒) redis.setex(f"product:{product_id}", 3600, json.dumps(product)) return product

2. 書き込みフロー(キャッシュの無効化)

書き込み側では「キャッシュを削除する」のが定石だ。DBに書いた後にキャッシュを更新しようとすると、更新前の古いデータが一瞬残る競合が起きやすい。

def update_product(product_id, data): # 1. DBを更新 db.execute("UPDATE products SET ... WHERE id = %s", product_id) # 2. キャッシュを削除(次のReadでCache Missとして再取得させる) redis.delete(f"product:{product_id}")

3. Cache-Asideの利点と欠点

・利点: アクセスされるデータだけがキャッシュに乗るため、メモリ効率が高い。実装がシンプルでDBとキャッシュの結合が疎なため、キャッシュが落ちてもDBに直接フォールバックする。
・欠点: 起動直後やキャッシュクリア後は全リクエストがDBに向かう「コールドスタート」が発生する。大量のCache Missが同時に発生する「キャッシュスタンピード」も注意が必要だ(後述)。
・向いているケース: 読み取りが多く書き込みが少ない参照マスターデータ(商品情報・カテゴリ一覧・設定値など)。

Write-Throughパターン

Write-Throughは「書き込み時にキャッシュとDBを同時に更新する」モデルだ。アプリケーションはキャッシュに書き込み、キャッシュ層がDBへの書き込みを同期的に実行する。または、アプリケーション側でキャッシュとDBの両方に書くケースもある。

1. 書き込みフロー

def update_user_profile(user_id, data): # 1. DBを更新(トランザクション内) db.execute("UPDATE users SET ... WHERE id = %s", user_id) # 2. キャッシュも即座に更新(削除ではなく上書き) redis.setex(f"user:{user_id}", 3600, json.dumps(data)) # 書き込みが1ステップ増えるがキャッシュが常に最新状態を保つ

2. Write-Throughの利点と欠点

・利点: キャッシュとDBが常に一致しているため、読み取り側の一貫性が高い。Cache-Asideと組み合わせると、初回Read後はWrite-Throughで常に最新データをキャッシュに保てる。
・欠点: 書き込みごとにキャッシュへの更新処理が加わるため、書き込みレイテンシが増加する。さらに、書いたデータが一度も読まれないまま期限切れになることも多く、使われないデータでキャッシュが埋まる問題が発生する。
・向いているケース: ユーザープロファイル・セッション情報など、書いた後に必ず読まれることが多いデータ。

Write-Back(Write-Behind)パターン

Write-Backは「まずキャッシュにだけ書いて、DBへの反映は後回しにする」モデルだ。Write-Behindとも呼ばれ、書き込みが非常に多いワークロード(IoTのセンサーデータ・リアルタイムカウンターなど)に使われる。

1. 書き込みフロー

アプリケーションはキャッシュに書くだけでレスポンスを返す。バックグラウンドの非同期プロセスが定期的(例: 30秒ごと)にキャッシュの変更分をDBに反映する。

# Write-Back: 書き込みはキャッシュのみ。DBへはバックグラウンドで反映 def increment_view_count(article_id): # キャッシュのカウンターをインクリメント(ミリ秒レベルで返る) redis.incr(f"views:{article_id}") # 別プロセス(Lambda or Cron)がDB反映を担当 def flush_view_counts_to_db(): keys = redis.keys("views:*") for key in keys: article_id = key.split(":")[1] count = redis.getdel(key) # 取得してキャッシュから削除 db.execute( "UPDATE articles SET views = views + %s WHERE id = %s", count, article_id )

2. Write-Backの利点と欠点

・利点: 書き込みレスポンスが最速(キャッシュへの書き込みだけで済む)。DBへの書き込み回数を大幅に削減でき、DB負荷を抑えられる。
・欠点: キャッシュが落ちた場合(ElastiCacheノード障害など)、まだDBに反映されていないデータが消える。データ損失リスクが許容できないユースケースには向かない。非同期反映の間はDBとキャッシュが不整合な状態になる。
・向いているケース: ページビューカウンター・いいね数・ゲームのスコアなど、多少のデータ欠損が許容できる集計系データ。金融トランザクションや在庫管理には使わない。

AWSでのキャッシュ実装:ElastiCacheとMemoryDB

AWSには主に2つのManaged Redisサービスがある。用途によって使い分けが必要だ(より詳しい比較はAmazon MemoryDB for Redisの入門記事で解説している)。

1. Amazon ElastiCache(Valkey/Redis)

ElastiCacheはキャッシュ用途に最適化された揮発性ストアだ。ノード障害時のデータ保証はないが、その代わりレイテンシが低くコストも抑えられる。Cache-AsideとWrite-Throughには最適だ。

・Cache-Aside向け設定: TTLはアクセスパターンに合わせて設定する。商品マスターなら3,600秒程度が定番だが、価格情報など変動が激しいものは短めに(300秒以下)設定する。
・Write-Throughと組み合わせる場合: ElastiCache for Redisのレプリケーショングループを使えば、Primaryへの書き込みが自動的にReplicaへ同期される。
・料金(2026年9月時点): cache.t4g.microが約0.016USD/時。東京リージョン(ap-northeast-1)での費用は月額約11.50USD程度から(シングルノード)。

2. Amazon MemoryDB for Redis

MemoryDBはデータを複数のAZにまたがるトランザクションログに永続化する。ElastiCacheと異なりノード障害後もデータが復元される。

Write-Backパターンを実装するなら、MemoryDBが適している。バックグラウンドでDBに書き込む前にキャッシュが落ちても、MemoryDBであればデータを復元できるためだ。ただし料金はElastiCacheの2~3倍になる点に注意が必要だ。

3. Lambda + ElastiCacheの接続管理

Lambda関数からElastiCacheに接続する場合は接続数の管理に注意が必要だ。Lambdaは並列実行されるたびに新しい接続を張るため、Redisの接続数上限(デフォルト65,535接続)に達する可能性がある。RDSと同様に(Amazon RDS Proxy入門記事参照)、接続プールの管理はサーバーレス環境では特に意識が必要だ。

Lambda + ElastiCacheでは接続をグローバル変数に持ち、実行環境が再利用される間は接続を使い回す実装が定番だ。

# Lambda: 接続をグローバルに保持して再利用する(Python + redis-py) import redis, json # ハンドラの外で定義 → 実行環境の再利用時に接続を維持 _redis_client = None def get_redis(): global _redis_client if _redis_client is None: _redis_client = redis.Redis( host="your-cluster.cache.amazonaws.com", port=6379, decode_responses=True ) return _redis_client def lambda_handler(event, context): client = get_redis() # キャッシュ操作...

Azureでのキャッシュ実装:Azure Cache for Redis

AzureではAzure Cache for RedisがManaged Redisの標準サービスだ(SKUの選び方についてはAzure Cache for Redis入門記事で詳しく解説している)。

各パターンとAzure Cache for Redisの関係を整理する。

・Cache-Aside: BasicおよびStandard SKUで問題なく実装できる。キャッシュが落ちた場合のフォールバック先はAzure SQL DatabaseやCosmos DBになる。
・Write-Through: AzureではWrite-Throughを「Azure Cache for Redis + Azure Functions」で実装するパターンが多い。Redisに書いたタイミングでAzure Functionsがトリガーされ、Cosmos DBやAzure SQL Databaseへ同期する構成が取れる。
・Write-Back: Enterpriseプランを使えばRedisのRDB/AOF永続化が有効になり、Write-Backのデータ損失リスクを軽減できる。ただし料金が大幅に跳ね上がる(2026年9月時点でEnterprise E5が約0.554USD/時程度)。

1. Azure Cache for Redisの接続設定

AzureではSSL接続がデフォルトで有効だ。ポートは6380(TLS)を使う。Basic/Standard SKUではAzure Private Link経由でVNet内に閉じることを推奨する(Azure Private Link入門記事参照)。

# Azure Cache for Redis 接続(C# / StackExchange.Redis) using StackExchange.Redis; using System.Text.Json; // シングルトンで接続を保持(再接続コストを避ける) private static readonly ConnectionMultiplexer _connection = ConnectionMultiplexer.Connect( "your-cache.redis.cache.windows.net:6380,password=YOUR_KEY,ssl=True" ); // Cache-Aside パターン public async Task GetProductAsync(string productId) { var db = _connection.GetDatabase(); var cached = await db.StringGetAsync($"product:{productId}"); if (cached.HasValue) return JsonSerializer.Deserialize(cached); var product = await _dbContext.Products.FindAsync(productId); await db.StringSetAsync( $"product:{productId}", JsonSerializer.Serialize(product), TimeSpan.FromHours(1) ); return product; }

パターン選択の判断フロー

実務で「どのパターンを使うか」迷ったときは次の判断軸で整理するといい。

判断軸 Cache-Aside Write-Through Write-Back
Read/Writeの比率 Readが多い(7:3以上) Read/Writeが同程度 Writeが多い(バーストあり)
データ欠損の許容 許容しない 許容しない ある程度許容できる
実装の複雑さ 低い 中程度 高い(非同期処理の管理が必要)
書き込みレイテンシ DB単体と同等 DB単体より若干高い 最速(キャッシュのみ)
典型的なユースケース 商品マスター・ユーザー情報参照 セッション管理・ランキング PV/いいね数・センサーデータ

一つのシステムで複数のパターンを使い分けることは珍しくない。たとえば「商品マスターはCache-Aside・セッションはWrite-Through・アクセスカウンターはWrite-Back」といった設計がよく見られる。

よくある落とし穴と対処法

1. キャッシュスタンピード(Thundering Herd)

Cache-AsideでTTLが切れた瞬間に大量のリクエストが同時にDBへ向かう問題だ。負荷が高いタイミングに重なると、DBがCPUスパイクで応答不能になることがある。

対処法の一つが「Mutex(分散ロック)」を使って最初の1リクエストだけにDB取得を許可し、他はロック解放を待たせる方法だ。RedisのSET NXコマンドを使えばシンプルな分散ロックが実装できる。

# Redisを使った分散ロックでスタンピード防止(Python 疑似コード) import time, json def get_product_with_lock(product_id): cache_key = f"product:{product_id}" lock_key = f"lock:{product_id}" cached = redis.get(cache_key) if cached: return json.loads(cached) # ロック取得(10秒のNX SET) acquired = redis.set(lock_key, "1", nx=True, ex=10) if acquired: try: product = db.query("SELECT * FROM products WHERE id = %s", product_id) redis.setex(cache_key, 3600, json.dumps(product)) return product finally: redis.delete(lock_key) else: # ロック取れなかったら短いリトライ後に再試行 time.sleep(0.1) return get_product_with_lock(product_id)

2. キャッシュポイズニング(誤ったデータのキャッシュ)

DBクエリがエラーやNULLを返したときに、そのエラー状態をキャッシュに書いてしまう問題だ。エラーをキャッシュしてしまうと、DBが復旧した後もエラーレスポンスをキャッシュから返し続ける。必ず「DBから正常なデータが取得できた場合のみキャッシュに書く」ロジックにすること。空振りキャッシュ(Negative Cache)を意図的に使う場合は、TTLを短く(30秒程度)設定するのが鉄則だ。

3. キャッシュキーの設計ミス

クラウド移行直後に起きやすいのが「キャッシュキーのコリジョン」だ。開発環境・ステージング・本番が同じElastiCacheに接続していると、異なる環境のデータが混在する。環境名をキーのプレフィックスに必ず含めること(例: prod:product:123・staging:product:123)。マルチテナントSaaSではテナントIDもキーに含め、テナント間のデータ混在を防ぐ。

4. TTL設計とデータ整合性の期待値

キャッシュは「DBより古いデータを返す可能性がある」という前提で設計を組む必要がある。「このAPIは常に最新データを返す」という要件がある場合、Cache-Asideは向いていない。データ整合性モデルの記事で解説しているように、クラウドシステムではCAP定理に基づいた整合性レベルの選択が設計の基本になる。

TTLを長くするほどキャッシュ効率は上がるが、データの鮮度は下がる。「商品説明は1時間古くてもいいが、在庫数は5分以内に反映したい」といった形でデータの種類ごとにTTLを分けるのが現場のベストプラクティスだ。

キャッシュ設計パターン入門|Cache-Aside・Write-Through・Write-BackをAWSとAzureで実装するアーキテクチャ判断ガイド - まとめ

本記事のまとめ

クラウドアーキテクチャにおける3大キャッシュパターンの要点を整理する。

・Cache-Aside(Lazy Loading): 読み取りが多いケースに適した基本パターン。実装がシンプルで、キャッシュが落ちても自動でDBにフォールバックする。キャッシュスタンピードへの対処は必須。
・Write-Through: 書き込み時にキャッシュとDBを同時更新することで高い一貫性を保つ。書き込みレイテンシの増加は許容した上で採用を判断する。
・Write-Back: 書き込み性能が最優先のユースケース向け。データ損失リスクがあるため、欠損が許容できるデータにのみ使う。
・AWS実装: キャッシュ用途にElastiCache(Valkey/Redis)、データ永続化が必要ならMemoryDBを選択する。Lambda連携では接続のグローバル保持が重要。
・Azure実装: Azure Cache for Redisでは必ずTLS(ポート6380)を使用し、Private Link経由でVNet内に閉じることを推奨する。

パターンは組み合わせて使うことが多い。まず「Cache-Aside」から始め、書き込み後の一貫性問題が出たら「Write-Through」を追加し、高負荷の集計系は「Write-Back」へ分離するという段階的なアプローチが現場では現実的だ。

PR

AWSクラウドネイティブデザインパターン(技術評論社)

キャッシュ戦略を含むクラウド設計パターンをAWS実装例で体系的に解説。複数サービスを組み合わせた実践的なアーキテクチャを学べる一冊。

関連記事をもっと読む

同じテーマの記事をまとめています。あわせて読みたい記事はこちらからご覧いただけます。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

目次