前回【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層アーキテクチャでは、各コンテナが明確な単一責務を持っています。
- ① 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 サービスを追加した完全版コードを見てみましょう。
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:
設定の注目ポイント
redis:7-alpineの採用: Ubuntuベースのイメージと比べ、わずか約30MBと超軽量でセキュリティフットプリントも最小限です。command: redis-server --appendonly yes: デフォルトのスナップショット保存(RDB)に加え、書き込みログを追記保存するAOF(Append-Only File)を有効化し、予期せぬコンテナ停止時もキャッシュデータを保持します。- 内部DNS解決(
ConnectionStrings__Redis=redis:6379): Docker Composeの内部DNSにより、Web APIコンテナからはサービス名のredisという名前で自動的にIPアドレスが解決されます。 - healthcheck(
redis-cli ping): Redisがリクエストに応答できる(PONGを返す)状態を検知してからWeb APIを起動させます。
④ C# ASP.NET Core での分散キャッシュ設計(IDistributedCache)
C#でキャッシュを扱う際、昔ながらの IMemoryCache と、モダンな IDistributedCache には決定的な構造の違いがあります。
| 比較項目 | 単一インメモリ(IMemoryCache) | Redis分散キャッシュ(IDistributedCache) |
|---|---|---|
| 保存場所 | Webアプリコンテナの内部プロセスヒープメモリ | 独立したRedisコンテナ |
| 複数コンテナ(スケール時) | コンテナごとにキャッシュが孤立し不整合発生 | 全コンテナで100%同一キャッシュを共有 |
| Webコンテナ再起動時 | キャッシュデータがすべて蒸発・消失 | Redis側にデータが温存され即復旧 |
| 標準インターフェース | IMemoryCache |
IDistributedCache |
ASP.NET Coreは、分散キャッシュのアクセス手段として IDistributedCache インターフェースを提供しています。
このインターフェースを使うことで、アプリケーションコードは「バックエンドがRedisなのか、SQL Serverなのか、テスト用インメモリなのか」を意識することなく、依存性の注入(DI)を通じて透過的にキャッシュを扱えるようになります。
NuGetパッケージの導入とDI登録
まずは公式の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 パターン」の処理フローを図解で追ってみましょう。
C# サービス層での Cache-Aside 実装コード
商品マスタを取得するサービスクラス CatalogService.cs を実装します。JSONシリアライズと IDistributedCache の非同期メソッドを組み合わせます。
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コンテナ内にキャッシュが保存されているか確認してみましょう。
$ 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:7-alpine と書くだけで5秒後に動き出す!インフラ調達の待ち時間なんて文字通りゼロだぜ!」
IMemoryCache を IDistributedCache に差し替えるだけで、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ミリ秒台の超高速レスポンスを叩き出す。
『docker-compose.yml のGit共有と .env のGit除外ルール』〜機密情報を安全に分離し、チーム開発で安全にリポジトリ共有するベストプラクティス〜
コメント
コメントを投稿