𝕏 f B! L
案件・求人数 12,345
案件を探す(準備中) エージェントを探す(準備中) お役立ち情報 ログイン
案件・求人数 12,345
AWS ElastiCache & Redis入門|SESエンジニア向けキャッシュ戦略実践ガイド

AWS ElastiCache & Redis入門|SESエンジニア向けキャッシュ戦略実践ガイド

AWSElastiCacheRedisSESエンジニア
目次
⚡ 3秒でわかる!この記事のポイント
  • ElastiCacheはDBの負荷を80%以上削減できるキャッシュサービス
  • Redis/Valkey対応でセッション管理・リアルタイムランキングにも活用
  • キャッシュ戦略の理解はバックエンド案件で必須スキル

「APIのレスポンスが遅い…DBへの負荷を減らしたい」「セッション管理をどこに持たせるべきかわからない」「Redisの経験が求められる案件が増えている」

AWS ElastiCacheは、インメモリキャッシュによりアプリケーションのパフォーマンスを劇的に改善するマネージドサービスです。SESのバックエンド案件では、Redisの実務経験を求められることが急増しています。

この記事では、AWS完全攻略シリーズEp.14として、ElastiCacheの基礎からキャッシュ戦略の実装まで、SESエンジニアが現場で即活用できる知識を体系的に解説します。

この記事でわかること
  • ElastiCacheの基本概念とアーキテクチャ
  • Redis vs Memcached の選択基準
  • キャッシュ戦略のパターンと実装
  • セッション管理の設計
  • パフォーマンスチューニング

AWS ElastiCacheとは

AWS ElastiCacheによるキャッシュ戦略の全体像

ElastiCacheは、AWSが提供するフルマネージドのインメモリデータストアです。データベースへのクエリ結果やAPIレスポンスをメモリ上にキャッシュすることで、レスポンスタイムをミリ秒単位に短縮できます。

ElastiCacheの主な特徴

特徴内容
マネージドパッチ適用・バックアップ・フェイルオーバーが自動
高速マイクロ秒レベルのレイテンシ
スケーラブルクラスターモードで水平スケーリング
高可用性Multi-AZ対応で自動フェイルオーバー
エンジンRedis OSS / Valkey / Memcached

Redis vs Memcached: どちらを選ぶ?

比較項目RedisMemcached
データ構造String, Hash, List, Set, Sorted SetString のみ
永続化あり(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

コスト削減のポイント

  1. リザーブドノードで最大55%割引
  2. Gravitonインスタンス(r7g)で約20%のコスト削減
  3. 適切なTTL設定でメモリを効率的に利用
  4. クラスターモードの活用で水平スケーリング

SES案件で求められるスキル

必須スキルレベル

レベル1(基礎): Redisの基本操作、キャッシュの概念理解
レベル2(実践): Cache-Asideパターンの実装、TTL設計
レベル3(応用): Pub/Sub、Sorted Set、レート制限の実装
レベル4(設計): クラスター設計、障害対策、パフォーマンスチューニング

単価への影響

  • Redis/キャッシュ設計経験あり → 月額5〜10万円アップ
  • ElastiCache運用経験 → インフラ案件で優遇
  • パフォーマンスチューニング → 高単価案件(月額70〜90万円)へのパス

よくある質問

Q: ElastiCacheとRedis Cloudの違いは?

比較項目ElastiCacheRedis Cloud
運用主体AWSRedis社
マルチクラウド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完全攻略シリーズの他の記事も合わせてご覧ください:

SES案件をお探しですか?

SES記事をもっと読む →
🏗️

SES BASE 編集長

SES業界歴10年以上のメンバーが在籍する編集チーム。SES企業での営業・エンジニア経験、フリーランス独立経験を持つメンバーが、業界のリアルな情報をお届けします。

📊 業界データに基づく記事制作 🔍 IPA・経済産業省データ参照 💼 SES実務経験者が執筆・監修