スキップしてメイン コンテンツに移動

【Day 23】Redisキャッシュを追加した3層コンテナアーキテクチャ 〜Web API + SQL Server + Redis の分散キャッシュ・セッション管理をComposeで構築〜

Day 23 アイキャッチ
▲ Day 23: Redisキャッシュを追加した3層コンテナアーキテクチャ
🐳
受講生の皆さん、お疲れ様です! Docker/コンテナ実践講座の専任講師です。
前回【Day 22】では、C# ASP.NET Core Web API と Microsoft SQL Server という実務の王道2大サービスを、Docker Composeで美しく連携させる環境を構築しました。
そして本日【Day 23】は、ここに超高速インメモリKVS(Key-Value Store)である「Redis」を追加し、現代のエンタープライズWeb開発における標準形である「モダン3層コンテナアーキテクチャ」へ進化させます!
ミドルウェアが増えるたびにサーバー調達とセットアップ手順書に追われていた過去と決別し、YAMLの数行追加だけでハイパフォーマンスな分散キャッシュ環境を手に入れる快感をぜひ体験してください!

① 導入・本日の達成目標

「Web APIのレスポンスが遅い…原因を調べたら、同じマスタデータを何度もSQL Serverにクエリ発行していた」
「キャッシュを導入したいが、オンプレミス環境でRedisサーバーを別に1台立てて設定するのは承認も構築も大変すぎる…」
「C#の IMemoryCache でローカルメモリにキャッシュしたら、コンテナをスケールアウト(複数台稼働)させた瞬間にデータ不整合が発生した…」

Webシステムの規模が拡大するにつれ、データベース(RDBMS)へのアクセス集中は必ずシステムのボトルネックになります。
その特効薬となるのが「Redisによる分散キャッシュ」ですが、従来はRedisサーバーの構築・管理コストの高さから導入を躊躇する現場も少なくありませんでした。
しかし、Docker Composeを使えば、わずか数行のYAMLを追加するだけで本番同等のRedisコンテナが立ち上がり、C#の IDistributedCache を通じて極めてシンプルに分散キャッシュを実装できます!

本日の講義を修了すると、以下の3大目標を完全に達成できます。

  • モダン3層コンテナアーキテクチャ(Web API + Redis + SQL Server)の役割分担と設計原則を完全に理解する。
  • docker-compose.yml へのRedisサービス定義(ポート・ボリューム・内部DNS連携)を記述し、1コマンドで3層環境を立ち上げられる。
  • ASP.NET Coreの標準インターフェースである IDistributedCache(StackExchange.Redis) を用いて、実務標準の「Cache-Aside パターン」を堅牢に実装できる。

② モダン3層コンテナアーキテクチャの全貌(パラダイムシフト)

まずは、Web API、Redis、SQL Serverがどのように連携し、高速レスポンスと高い耐障害性を実現するのか、その全体アーキテクチャを図解で確認しましょう。

モダン3層コンテナアーキテクチャ
▲ 【図解①】モダン3層コンテナアーキテクチャ(Web API層 ➡ Redis分散キャッシュ層 ➡ SQL Server永続化層の役割分担)

この3層アーキテクチャでは、各コンテナが明確な単一責務を持っています。

  • ① Web API層(ASP.NET Core): クライアントからのHTTPリクエストを処理し、ビジネスロジックを実行するアプリケーション層。ステートレス(無状態)に設計され、負荷に応じて何台でも水平スケールアウト可能です。
  • ② Redis分散キャッシュ層: すべてのデータをメモリ上で保持する超高速KVS。頻繁に参照されるマスタデータやユーザーセッション情報をミリ秒未満(サブミリ秒)で即座に返却し、後段のRDBMSを守る盾となります。
  • ③ SQL Server永続化層: トランザクション整合性(ACID特性)を保証するリレーショナルデータベース。ディスク上のボリューム(Volume)にデータを安全に永続化します。

VM/物理サーバー時代との決定的な違い:セットアップ手順書の消滅

従来のオンプレミスや仮想マシン(VM)運用では、システムに「Redis」という新しいミドルウェアを追加するだけで、以下のような膨大な作業が発生していました。

  • 新規Linuxサーバーの調達・OSインストール・IPアドレス割り当て申請
  • yum や apt によるRedisパッケージのインストールと依存関係解決
  • /etc/redis/redis.conf の手動パラメータ調整(bind設定、protected-mode、メモリ上限設定など)
  • ホストPCや開発者ごとのファイアウォール開放設定と疎通確認

これがDocker Composeになると、「公式の redis:7-alpine イメージをYAMLに数行書くだけ」で完結します。
チームの全員が、新人のPCであってもCIサーバーであっても、docker compose up -d を打つだけで寸分違わぬ同一構成のRedisキャッシュ環境が5秒で立ち上がります。インフラ構成の変更が、Gitのプルリクエスト上でたった数行の差分としてレビュー可能になるのです!

③ docker-compose.yml へのRedisサービス完全定義

それでは、前回作成した docker-compose.yml に Redis サービスを追加した完全版コードを見てみましょう。

docker-compose.yml へのRedis追加
▲ 【図解②】docker-compose.yml へのRedisサービス追加とWeb API接続定義(内部DNSと環境変数注入)
# docker-compose.yml(Web API + SQL Server + Redis 3層連携)
services:
  db:
    image: mcr.microsoft.com/mssql/server:2022-latest
    container_name: my-sqlserver
    environment:
      - ACCEPT_EULA=Y
      - MSSQL_SA_PASSWORD=YourStrong@Pass123
    ports:
      - "1433:1433"
    volumes:
      - sqldata:/var/opt/mssql
    healthcheck:
      test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P 'YourStrong@Pass123' -C -Q 'SELECT 1' || exit 1"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 15s

  redis:
    image: redis:7-alpine  # 軽量Linux Alpineベースの公式Redisイメージ(約30MB)
    container_name: my-redis
    command: redis-server --appendonly yes  # AOF(Append-Only File)永続化を有効化
    ports:
      - "6379:6379"  # ホストPCのRedisInsightやGUIツールからの接続用
    volumes:
      - redisdata:/data  # キャッシュデータの永続化領域
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 3

  webapi:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: my-webapi
    ports:
      - "5000:8080"
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - ConnectionStrings__DefaultConnection=Server=db,1433;Database=CatalogDb;User Id=sa;Password=YourStrong@Pass123;TrustServerCertificate=True;
      - ConnectionStrings__Redis=redis:6379  # 内部DNS名 'redis' を指定!
    depends_on:
      db:
        condition: service_healthy  # SQL Serverの初期化完了を待機
      redis:
        condition: service_healthy  # Redisのping応答確認を待機

volumes:
  sqldata:
  redisdata:

設定の注目ポイント

  1. redis:7-alpine の採用: Ubuntuベースのイメージと比べ、わずか約30MBと超軽量でセキュリティフットプリントも最小限です。
  2. command: redis-server --appendonly yes: デフォルトのスナップショット保存(RDB)に加え、書き込みログを追記保存するAOF(Append-Only File)を有効化し、予期せぬコンテナ停止時もキャッシュデータを保持します。
  3. 内部DNS解決(ConnectionStrings__Redis=redis:6379): Docker Composeの内部DNSにより、Web APIコンテナからはサービス名の redis という名前で自動的にIPアドレスが解決されます。
  4. healthcheck(redis-cli ping): Redisがリクエストに応答できる(PONGを返す)状態を検知してからWeb APIを起動させます。

④ C# ASP.NET Core での分散キャッシュ設計(IDistributedCache)

C#でキャッシュを扱う際、昔ながらの IMemoryCache と、モダンな IDistributedCache には決定的な構造の違いがあります。

メモリキャッシュ vs Redis分散キャッシュ
▲ 【図解③】単一インメモリキャッシュ(IMemoryCache)の限界 vs Redis分散キャッシュ(IDistributedCache)のスケール耐性
比較項目 単一インメモリ(IMemoryCache) Redis分散キャッシュ(IDistributedCache)
保存場所 Webアプリコンテナの内部プロセスヒープメモリ 独立したRedisコンテナ
複数コンテナ(スケール時) コンテナごとにキャッシュが孤立し不整合発生 全コンテナで100%同一キャッシュを共有
Webコンテナ再起動時 キャッシュデータがすべて蒸発・消失 Redis側にデータが温存され即復旧
標準インターフェース IMemoryCache IDistributedCache

ASP.NET Coreは、分散キャッシュのアクセス手段として IDistributedCache インターフェースを提供しています。
このインターフェースを使うことで、アプリケーションコードは「バックエンドがRedisなのか、SQL Serverなのか、テスト用インメモリなのか」を意識することなく、依存性の注入(DI)を通じて透過的にキャッシュを扱えるようになります。

NuGetパッケージの導入とDI登録

まずは公式のRedisプロバイダーパッケージを追加します。

$ dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis

次に Program.cs で、環境変数から取得した接続文字列を使ってRedisキャッシュサービスを登録します。

// Program.cs: Redis分散キャッシュの登録
var redisConnectionString = builder.Configuration.GetConnectionString("Redis")
    ?? "localhost:6379";

builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = redisConnectionString;
    options.InstanceName = "DockerDemo_";  // キーのプレフィックス(名前空間の衝突防止)
});

⑤ 実践ハンズオン:Cache-Aside パターン実装と動作検証

分散キャッシュ運用のデファクトスタンダードである「Cache-Aside パターン」の処理フローを図解で追ってみましょう。

Cache-Aside パターンの実践フロー
▲ 【図解④】Cache-Aside パターンの3ステップフロー(Redis確認 ➡ DB取得 ➡ TTL付きキャッシュ書き込み)

C# サービス層での Cache-Aside 実装コード

商品マスタを取得するサービスクラス CatalogService.cs を実装します。JSONシリアライズと IDistributedCache の非同期メソッドを組み合わせます。

using System.Text.Json;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.EntityFrameworkCore;

public class CatalogService
{
    private readonly AppDbContext _dbContext;
    private readonly IDistributedCache _cache;
    private readonly ILogger<CatalogService> _logger;

    public CatalogService(AppDbContext dbContext, IDistributedCache cache, ILogger<CatalogService> logger)
    {
        _dbContext = dbContext;
        _cache = cache;
        _logger = logger;
    }

    public async Task<ProductDto?> GetProductByIdAsync(int id)
    {
        string cacheKey = $"product:{id}";

        // ① まずRedisキャッシュを確認
        string? cachedJson = await _cache.GetStringAsync(cacheKey);
        if (!string.IsNullOrEmpty(cachedJson))
        {
            _logger.LogInformation("⚡ [キャッシュヒット] Redisから即座に返却: Key={Key}", cacheKey);
            return JsonSerializer.Deserialize<ProductDto>(cachedJson);
        }

        // ② キャッシュミス時:SQL Serverから最新データをクエリ取得
        _logger.LogWarning("⚠️ [キャッシュミス] SQL Serverへクエリ発行: Id={Id}", id);
        var product = await _dbContext.Products.FindAsync(id);
        if (product == null) return null;

        var dto = new ProductDto(product.Id, product.Name, product.Price);

        // ③ 取得したデータをTTL(有効期限)付きでRedisに書き込み
        var cacheOptions = new DistributedCacheEntryOptions()
            .SetAbsoluteExpiration(TimeSpan.FromMinutes(10))  // 10分後に自動破棄
            .SetSlidingExpiration(TimeSpan.FromMinutes(2));   // 2分アクセスが無ければ破棄

        await _cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(dto), cacheOptions);
        _logger.LogInformation("💾 Redisにキャッシュ保存完了: Key={Key}", cacheKey);

        return dto;
    }
}

動作確認:Redis CLI でのキャッシュ検証

コンテナを一括起動して、実際にRedisコンテナ内にキャッシュが保存されているか確認してみましょう。

# 3層コンテナを一括起動
$ docker compose up -d --build

# 初回リクエスト(キャッシュミス ➡ SQL Serverから取得&Redis保存)
$ curl http://localhost:5000/api/products/1

# Redisコンテナ内部に入り、保存されたキーを確認
$ docker compose exec redis redis-cli KEYS "*"
1) "DockerDemo_product:1"

# キャッシュの残り有効秒数(TTL)を確認
$ docker compose exec redis redis-cli TTL "DockerDemo_product:1"
(integer) 598

2回目以降のリクエストでは、SQL Serverへのクエリログは一切出力されず、Redisからわずか1ミリ秒未満で即座にJSONが返却されるようになります!

⑥ YouTube動画連動:霊夢と魔理沙の掛け合い解説

本講義の内容は、YouTubeの動画講義(ゆっくり解説)でも楽しくビジュアルに学べます!
3層アーキテクチャの劇的な快適さを、霊夢と魔理沙の会話で振り返ってみましょう。

霊夢: 「魔理沙!私、オンプレ時代にRedisを導入しようとして、インフラ部門に申請出したら『サーバー調達に1ヶ月かかります』って言われて諦めたトラウマがあるのよね…」
魔理沙: 「全エンジニアが泣いたオンプレあるあるだな!だがDocker Composeなら、YAMLに redis:7-alpine と書くだけで5秒後に動き出す!インフラ調達の待ち時間なんて文字通りゼロだぜ!」
霊夢: 「しかもC#のコードも、IMemoryCache を IDistributedCache に差し替えるだけで、Webコンテナを何台に増やしてもデータがピシッと共有されるのが感動的ね!」
魔理沙: 「そうだ!Cache-AsideパターンでDBの手前にRedisを置くだけで、SQL Serverのクエリ負荷は激減してレスポンスも1ミリ秒台になる!Web API、SQL Server、Redisの3大コンテナ連携こそ、モダンWeb開発の真骨頂だぜ!」

⑦ 実務でありがちな落とし穴&ベストプラクティス

💡 現場で必ず遭遇する Redis 運用の4大落とし穴

落とし穴① キャッシュの有効期限(TTL)を設定せずメモリ枯渇(OOM)を起こす
Redisはインメモリデータベースのため、物理メモリ上限に達すると新規データの書き込みエラー(OOM command not allowed)が発生します。
キャッシュを登録する際は、必ず DistributedCacheEntryOptions で AbsoluteExpirationRelativeToNow を指定し、古いデータが自動的に失効・解放されるように設計してください。

落とし穴② キャッシュスタンピード(キャッシュ失効時のDB負荷集中)
人気商品のデータなど、毎秒数百リクエストが届くキーのTTLが切れた瞬間、全リクエストが同時にSQL Serverへ殺到してDBサーバーが過負荷でダウンすることがあります。
これを防ぐため、有効期限に数秒〜数十秒のランダムな「ジッター(揺らぎ)」を加えるか、セマフォによる二重取得防止ロック(SingleFlightパターン)を検討しましょう。

落とし穴③ 接続文字列に localhost を指定してしまう罠
ホストPCでの開発感覚で ConnectionStrings:Redis=localhost:6379 と書いてしまうと、Web APIコンテナ自身の内部を探してしまい SocketFailure で接続できません。
コンテナ間通信では、必ずComposeのサービス名である redis:6379 を指定してください。

落とし穴④ シリアライズの非同期処理とメモリオーバーヘッド
IDistributedCache はバイト配列(byte[])または文字列(string)を扱います。
巨大なオブジェクトコレクションを安易にJSONシリアライズしてRedisに格納すると、シリアライズ処理のCPU負荷とメモリ消費が増大します。キャッシュするデータは画面描画に必要な最小限のDTOに絞り込みましょう。

📌 本日のまとめ & 明日へのステップ
  • モダン3層アーキテクチャ: Web API(ロジック) + Redis(高速キャッシュ) + SQL Server(永続化)の3層により、高スループットとデータの安全性を両立する。
  • docker-compose.yml へのRedis追加: 公式の軽量イメージ(redis:7-alpine)を指定し、redisdata ボリュームとAOF永続化で安全なキャッシュ基盤を1コマンドで構築する。
  • IDistributedCache による疎結合設計: C#標準のインターフェースを活用し、Webコンテナの水平スケールアウトや再起動にびくともしない分散キャッシュを実現する。
  • Cache-Aside パターンの実践: 「Redis確認 ➡ DB取得 ➡ TTL付きキャッシュ保存」の3ステップにより、SQL Serverの負荷を最小化し、1ミリ秒台の超高速レスポンスを叩き出す。
次回【Day 24】予告:
『docker-compose.yml のGit共有と .env のGit除外ルール』〜機密情報を安全に分離し、チーム開発で安全にリポジトリ共有するベストプラクティス〜

コメント

このブログの人気の投稿

【Perfume】Perfume 3rd Tour 「JPN」さいたまスーパーアリーナに行って参りました~

Perfume 3rd Tour 「JPN」1月28日、29日の2daysで参戦して参りました。 ファンクラブ会員の選考抽選会に28日、29日の両日で申し込んでいたんですが、両方とも当選していたという嬉しい誤算。 仕事だぁ、なんだかんだと紆余曲折ありまして28日の参戦は危ぶまれていたのですが、なんとかかんとか28日は40分遅刻しながらも参戦、29日はフル参戦する事が出来ました(∩´∀`)∩

Perfume、2025年末で“コールドスリープ(活動休止)”へ──東京ドーム2daysの記録と、公式・各界・ファンの声

  1) 公式発表・基本情報 活動休止(コールドスリープ)を正式発表 2025年9月21日、公式サイト「 Perfumeより 」で、2026年から一度コールドスリープに入ると発表。再始動の意思(“新しいPerfume”として戻る)も明記。 Perfume Official Site 今後の運用告知(配信・上映など) 同21日付の「 今後のPerfumeに関して 」で、9/23(火・祝)東京ドーム公演の LIVESHIP全世界生配信 および 全国ディレイビューイング 決定を案内。特設サイトも併記。 Perfume Official Site +1 公式グッズ **「Perfume Calendar 2026」**発売決定(B2壁掛け/表紙+2カ月1枚×6枚+ポスター)。テーマは“フルーツ”。 Perfume Official Site 2) 東京ドーム2days(9/22・23)要点 公演タイトル: ZO/Z5 Anniversary “ネビュラロマンス” Episode TOKYO DOME 。日程・開場/開演・会場・物販等の実務情報は特設で公開。 Perfume Official Site 9/23公演を世界配信(LIVESHIP) /ディレイビューイング実施。 Perfume Official Site 海外向け報道(英語)も「 indefinite hiatus 」として広く配信。 The Times of India 国内英字メディアも速報。 朝日新聞 3) 現地レポ( メリ爺 )から見える空気 あなたの現地記録(9/24公開)を、ファクトと現場情景の両面で反映: 両日参戦 。22日は アリーナ 、23日は 1階席から俯瞰 。前日の発表を受け、開演前のザワつき→MC後に“いつもの一体感”へ。開演は22日 18:45 頃。オープニングは**P³ツアー東京ドーム(2020/2)**と同趣向で“回収”の演出。 メリ爺のゲーム万歳 MC所感 :解散ではなく前向きな休止で不安を和らげる内容。 かしゆか の涙、 のっち の気丈さ、 あ〜ちゃん の思い出語り──3人の結束と「 また戻る 」約束が強く刻まれた。 メリ爺のゲーム万歳 体感時間 : “3時間弱”級のボリューム ながら“過去一...

ニンテンドースイッチでオンラインゲームを遊ぶには?

モンスターハンターライズが発売になって、家族でモンハン楽しみたい人が増えたみたいですね。 某ブログの検索キーワードを調べると、モンハンを家族でやるにはどうしたらいいのか?を調べている人が沢山いるのがわかります。 ※これでも一部  家族で協力するにはSwitch2台、ソフトも2つ購入する必要があります。 1台のスイッチでは遊べませんし、ソフト1つでも遊べません。 でも、1台のスイッチで別々アカウントがあれば交代で遊ぶこともできますよ。セーブデータもしっかり分かれます。 Switchのオンラインゲームを家族で遊ぶ方法については以下の記事にまとめてますのでよろしければ参考にしてみてくださいね。 家族でモンスターハンターライズDL版のオンラインプレーを楽しむ方法