- ElastiCacheはDBの負荷を80%以上削減できるキャッシュサービス
- Redis/Valkey対応でセッション管理・リアルタイムランキングにも活用
- キャッシュ戦略の理解はバックエンド案件で必須スキル
「APIのレスポンスが遅い…DBへの負荷を減らしたい」「セッション管理をどこに持たせるべきかわからない」「Redisの経験が求められる案件が増えている」
AWS ElastiCacheは、インメモリキャッシュによりアプリケーションのパフォーマンスを劇的に改善するマネージドサービスです。SESのバックエンド案件では、Redisの実務経験を求められることが急増しています。
この記事では、AWS完全攻略シリーズEp.14として、ElastiCacheの基礎からキャッシュ戦略の実装まで、SESエンジニアが現場で即活用できる知識を体系的に解説します。
- ElastiCacheの基本概念とアーキテクチャ
- Redis vs Memcached の選択基準
- キャッシュ戦略のパターンと実装
- セッション管理の設計
- パフォーマンスチューニング
AWS ElastiCacheとは

ElastiCacheは、AWSが提供するフルマネージドのインメモリデータストアです。データベースへのクエリ結果やAPIレスポンスをメモリ上にキャッシュすることで、レスポンスタイムをミリ秒単位に短縮できます。
ElastiCacheの主な特徴
| 特徴 | 内容 |
|---|---|
| マネージド | パッチ適用・バックアップ・フェイルオーバーが自動 |
| 高速 | マイクロ秒レベルのレイテンシ |
| スケーラブル | クラスターモードで水平スケーリング |
| 高可用性 | Multi-AZ対応で自動フェイルオーバー |
| エンジン | Redis OSS / Valkey / Memcached |
Redis vs Memcached: どちらを選ぶ?
| 比較項目 | Redis | Memcached |
|---|---|---|
| データ構造 | String, Hash, List, Set, Sorted Set | String のみ |
| 永続化 | あり(RDB/AOF) | なし |
| レプリケーション | あり | なし |
| Pub/Sub | あり | なし |
| Lua スクリプト | あり | なし |
| マルチスレッド | シングルスレッド | マルチスレッド |
| 用途 | キャッシュ + データストア | 純粋なキャッシュ |
結論: 迷ったらRedis(Valkey)を選択しましょう。SES案件で求められるのもほぼRedisです。
ElastiCacheの構築
コンソールからの作成
# AWS CLIでRedisクラスター作成
aws elasticache create-replication-group \
--replication-group-id my-app-cache \
--replication-group-description "App cache cluster" \
--engine redis \
--engine-version 7.1 \
--node-group-configuration \
"ReplicaCount=1,PrimaryAvailabilityZone=ap-northeast-1a,ReplicaAvailabilityZones=ap-northeast-1c" \
--cache-node-type cache.r7g.large \
--automatic-failover-enabled \
--multi-az-enabled \
--at-rest-encryption-enabled \
--transit-encryption-enabled \
--cache-subnet-group-name my-cache-subnet
Terraform/CDKでの構築
# Terraform: ElastiCache Redis クラスター
resource "aws_elasticache_replication_group" "app_cache" {
replication_group_id = "my-app-cache"
description = "Application cache cluster"
engine = "redis"
engine_version = "7.1"
node_type = "cache.r7g.large"
num_cache_clusters = 2
automatic_failover_enabled = true
multi_az_enabled = true
at_rest_encryption_enabled = true
transit_encryption_enabled = true
subnet_group_name = aws_elasticache_subnet_group.cache.name
security_group_ids = [aws_security_group.cache.id]
parameter_group_name = aws_elasticache_parameter_group.custom.name
# メンテナンスウィンドウ
maintenance_window = "sun:05:00-sun:06:00"
# スナップショット
snapshot_retention_limit = 7
snapshot_window = "03:00-04:00"
tags = {
Environment = "production"
Project = "my-app"
}
}
resource "aws_elasticache_parameter_group" "custom" {
name = "my-app-redis-params"
family = "redis7"
parameter {
name = "maxmemory-policy"
value = "allkeys-lru"
}
}
キャッシュ戦略パターン
Cache-Aside(Lazy Loading)
最も一般的なパターンで、アプリケーション側がキャッシュの管理を行います:
// Cache-Aside パターン実装
import Redis from 'ioredis';
const redis = new Redis({
host: 'my-app-cache.xxxxx.apne1.cache.amazonaws.com',
port: 6379,
tls: {}, // 暗号化通信
});
async function getUserById(userId: string): Promise<User> {
const cacheKey = `user:${userId}`;
// 1. キャッシュから取得を試みる
const cached = await redis.get(cacheKey);
if (cached) {
console.log('Cache HIT');
return JSON.parse(cached);
}
// 2. キャッシュミス → DBから取得
console.log('Cache MISS');
const user = await prisma.user.findUnique({ where: { id: userId } });
if (user) {
// 3. キャッシュに保存(TTL: 1時間)
await redis.setex(cacheKey, 3600, JSON.stringify(user));
}
return user;
}
Write-Through
書き込み時にDBとキャッシュを同時に更新:
async function updateUser(userId: string, data: UpdateUserDto): Promise<User> {
// 1. DBを更新
const user = await prisma.user.update({
where: { id: userId },
data,
});
// 2. キャッシュも即座に更新
const cacheKey = `user:${userId}`;
await redis.setex(cacheKey, 3600, JSON.stringify(user));
return user;
}
Write-Behind(Write-Back)
書き込みをキャッシュに行い、非同期でDBに反映:
// Write-Behind: 書き込みをバッファリングして一括DB反映
class WriteBehindCache {
private writeBuffer: Map<string, any> = new Map();
private flushInterval: NodeJS.Timer;
constructor(private redis: Redis, private flushMs: number = 5000) {
this.flushInterval = setInterval(() => this.flush(), flushMs);
}
async write(key: string, value: any) {
// キャッシュに即座に書き込み
await this.redis.setex(key, 3600, JSON.stringify(value));
// バッファに追加
this.writeBuffer.set(key, value);
}
private async flush() {
if (this.writeBuffer.size === 0) return;
const entries = Array.from(this.writeBuffer.entries());
this.writeBuffer.clear();
// バッチでDB更新
await prisma.$transaction(
entries.map(([key, value]) => {
const id = key.split(':')[1];
return prisma.user.update({ where: { id }, data: value });
})
);
}
}
キャッシュ無効化戦略
// イベント駆動のキャッシュ無効化
class CacheInvalidator {
constructor(private redis: Redis) {}
// 単一キーの無効化
async invalidate(key: string) {
await this.redis.del(key);
}
// パターンマッチで一括無効化
async invalidatePattern(pattern: string) {
const keys = await this.redis.keys(pattern);
if (keys.length > 0) {
await this.redis.del(...keys);
}
}
// ユーザー関連キャッシュの全無効化
async invalidateUser(userId: string) {
await this.invalidatePattern(`user:${userId}*`);
await this.invalidatePattern(`user-posts:${userId}*`);
await this.invalidatePattern(`user-profile:${userId}*`);
}
}
セッション管理
Express.jsでのRedisセッション
import session from 'express-session';
import RedisStore from 'connect-redis';
import Redis from 'ioredis';
const redis = new Redis({
host: process.env.REDIS_HOST,
port: 6379,
tls: {},
});
app.use(session({
store: new RedisStore({ client: redis }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: true,
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000, // 24時間
sameSite: 'lax',
},
}));
JWT + Redisのハイブリッド認証
// JWTのブラックリスト管理
class TokenBlacklist {
constructor(private redis: Redis) {}
// トークンを無効化(ログアウト時)
async revoke(token: string, expiresIn: number) {
await this.redis.setex(`blacklist:${token}`, expiresIn, '1');
}
// トークンが有効か確認
async isValid(token: string): Promise<boolean> {
const revoked = await this.redis.get(`blacklist:${token}`);
return !revoked;
}
}
Redis活用パターン
リアルタイムランキング(Sorted Set)
// ゲームやECサイトのランキング
class LeaderboardService {
constructor(private redis: Redis) {}
// スコア更新
async updateScore(userId: string, score: number) {
await this.redis.zadd('leaderboard', score, userId);
}
// トップN取得
async getTopN(n: number): Promise<Array<{ userId: string; score: number }>> {
const results = await this.redis.zrevrange('leaderboard', 0, n - 1, 'WITHSCORES');
const rankings = [];
for (let i = 0; i < results.length; i += 2) {
rankings.push({ userId: results[i], score: Number(results[i + 1]) });
}
return rankings;
}
// 特定ユーザーの順位取得
async getRank(userId: string): Promise<number | null> {
const rank = await this.redis.zrevrank('leaderboard', userId);
return rank !== null ? rank + 1 : null;
}
}
レート制限(Rate Limiting)
// スライディングウィンドウ方式のレート制限
class RateLimiter {
constructor(private redis: Redis) {}
async checkLimit(
key: string,
maxRequests: number,
windowMs: number
): Promise<{ allowed: boolean; remaining: number }> {
const now = Date.now();
const windowStart = now - windowMs;
const multi = this.redis.multi();
// 古いエントリを削除
multi.zremrangebyscore(key, 0, windowStart);
// 現在のリクエストを追加
multi.zadd(key, now, `${now}-${Math.random()}`);
// 現在のウィンドウ内のカウント
multi.zcard(key);
// TTL設定
multi.pexpire(key, windowMs);
const results = await multi.exec();
const count = results[2][1] as number;
return {
allowed: count <= maxRequests,
remaining: Math.max(0, maxRequests - count),
};
}
}
Pub/Subメッセージング
// リアルタイム通知配信
const subscriber = new Redis(process.env.REDIS_HOST);
const publisher = new Redis(process.env.REDIS_HOST);
// 購読側
subscriber.subscribe('notifications', 'chat-messages');
subscriber.on('message', (channel, message) => {
const data = JSON.parse(message);
if (channel === 'notifications') {
broadcastToWebSocket(data);
}
if (channel === 'chat-messages') {
handleChatMessage(data);
}
});
// 発行側
async function sendNotification(userId: string, content: string) {
await publisher.publish('notifications', JSON.stringify({
userId,
content,
timestamp: Date.now(),
}));
}
パフォーマンスチューニング
メモリ管理のベストプラクティス
# Redisメモリ使用状況の確認
redis-cli INFO memory
# 推奨設定
# maxmemory: 利用可能メモリの75%に設定
# maxmemory-policy:
# - allkeys-lru: 全キーからLRUで削除(キャッシュ用途に最適)
# - volatile-lru: TTLが設定されたキーからLRUで削除
# - noeviction: メモリ不足時にエラー返却(データストア用途)
接続プーリング
// ioredis接続プール設定
const redis = new Redis({
host: process.env.REDIS_HOST,
port: 6379,
// 接続設定
connectTimeout: 5000,
retryStrategy: (times) => {
if (times > 3) return null; // 3回で諦める
return Math.min(times * 200, 2000);
},
// パフォーマンス設定
enableReadyCheck: true,
lazyConnect: true,
keepAlive: 30000,
});
パイプラインによる一括操作
// 個別コマンド(遅い)
for (const key of keys) {
await redis.get(key); // N回のRTT
}
// パイプライン(速い)
const pipeline = redis.pipeline();
for (const key of keys) {
pipeline.get(key);
}
const results = await pipeline.exec(); // 1回のRTT
コスト最適化
インスタンスタイプ選定
| ユースケース | 推奨タイプ | 月額目安 |
|---|---|---|
| 開発/検証 | cache.t4g.micro | ~$15 |
| 小規模本番 | cache.r7g.large | ~$200 |
| 中規模本番 | cache.r7g.xlarge | ~$400 |
| 大規模本番 | cache.r7g.2xlarge | ~$800 |
コスト削減のポイント
- リザーブドノードで最大55%割引
- Gravitonインスタンス(r7g)で約20%のコスト削減
- 適切なTTL設定でメモリを効率的に利用
- クラスターモードの活用で水平スケーリング
SES案件で求められるスキル
必須スキルレベル
レベル1(基礎): Redisの基本操作、キャッシュの概念理解
レベル2(実践): Cache-Asideパターンの実装、TTL設計
レベル3(応用): Pub/Sub、Sorted Set、レート制限の実装
レベル4(設計): クラスター設計、障害対策、パフォーマンスチューニング
単価への影響
- Redis/キャッシュ設計経験あり → 月額5〜10万円アップ
- ElastiCache運用経験 → インフラ案件で優遇
- パフォーマンスチューニング → 高単価案件(月額70〜90万円)へのパス
よくある質問
Q: ElastiCacheとRedis Cloudの違いは?
| 比較項目 | ElastiCache | Redis Cloud |
|---|---|---|
| 運用主体 | AWS | Redis社 |
| マルチクラウド | AWSのみ | AWS/GCP/Azure |
| Redis Modules | 一部制限 | フルサポート |
| コスト | AWSインフラ込み | やや高め |
AWS中心の環境ならElastiCacheが最適です。
Q: Valkeyとは何?
Valkeyは、Redis Ltd.がライセンスをBSDからRSALv2/SSPLに変更したことを受けて、Linux Foundationがフォークしたオープンソースのインメモリデータストアです。ElastiCacheは2024年からValkeyエンジンもサポートしています。
Q: キャッシュの整合性はどう保つ?
キャッシュとDBの整合性を保つポイント:
- TTLを適切に設定して古いデータを自動失効
- Write-Throughで書き込み時にキャッシュも更新
- イベント駆動でデータ変更時にキャッシュを無効化
- 最終的整合性を許容する設計(多くのWebアプリでは問題なし)
まとめ
AWS ElastiCacheとRedisは、SESバックエンド案件で必須のスキルです。キャッシュ戦略の理解と実装経験は、単価アップに直結します。
- ElastiCacheはDBの負荷を大幅削減するマネージドキャッシュ
- Redis(Valkey)はキャッシュ+データストアとして多用途
- Cache-Aside/Write-Through等の戦略パターンを使い分ける
- セッション管理・ランキング・レート制限など活用範囲が広い
- Redis経験はSESバックエンド案件で高単価につながる
AWS完全攻略シリーズの他の記事も合わせてご覧ください: