Upstash Redis実践 - サーバーレスRedisでデータキャッシング

初級 | 15分 で読める | 2026.04.24

公式ドキュメント

この記事の要点

Upstash Redisでサーバーレス環境に最適化されたRedisを構築
• Next.js App Routerと統合し、エッジランタイムでキャッシュとレート制限を実装
REST APIモードでCloudflare Workers等でも利用可能

Upstash Redisとは

Upstash Redisは、サーバーレス環境向けに最適化されたRedis互換データベースです。従量課金制で、接続プーリング不要、HTTPベースのアクセスが可能です。

Upstashの特徴

機能説明
サーバーレス対応コールドスタートなし、接続管理不要
グローバルレプリケーション複数リージョンに自動レプリカ配置
REST APIHTTP経由でアクセス(エッジ対応)
従量課金使った分だけ課金、無料枠あり
Redis互換標準Redisコマンドをサポート

ポイント: Upstashは無料プランで1日10,000コマンド利用でき、個人プロジェクトに最適です。

プロジェクトセットアップ

# Next.jsプロジェクト作成
npx create-next-app@latest my-app --typescript --app

cd my-app

# Upstash Redisクライアントのインストール
npm install @upstash/redis
npm install @upstash/ratelimit

Upstashデータベース作成

  1. https://console.upstash.com/ にアクセス
  2. “Create Database” をクリック
  3. リージョン選択(us-east-1等、アプリケーションに近い場所)
  4. “TLS Enabled” を有効化
  5. “UPSTASH_REDIS_REST_URL” と “UPSTASH_REDIS_REST_TOKEN” を取得

環境変数

# .env.local
UPSTASH_REDIS_REST_URL=https://xxxxx.upstash.io
UPSTASH_REDIS_REST_TOKEN=xxxxx

実践メモ: REST APIモードはエッジランタイム(Vercel Edge、Cloudflare Workers)で動作します。

Redis クライアントの初期化

// lib/redis.ts
import { Redis } from '@upstash/redis';

export const redis = new Redis({
  url: process.env.UPSTASH_REDIS_REST_URL!,
  token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});

基本操作

文字列操作

// app/actions/cache.ts
'use server';

import { redis } from '@/lib/redis';

// セット
export async function setCache(key: string, value: string, ttl?: number) {
  if (ttl) {
    await redis.setex(key, ttl, value);
  } else {
    await redis.set(key, value);
  }
}

// ゲット
export async function getCache(key: string) {
  return await redis.get<string>(key);
}

// 削除
export async function deleteCache(key: string) {
  await redis.del(key);
}

// 複数セット
export async function setCacheMultiple(entries: Record<string, string>) {
  const pipeline = redis.pipeline();
  
  Object.entries(entries).forEach(([key, value]) => {
    pipeline.set(key, value);
  });
  
  await pipeline.exec();
}

JSON操作

// app/actions/user.ts
'use server';

import { redis } from '@/lib/redis';

interface User {
  id: string;
  name: string;
  email: string;
  createdAt: string;
}

export async function cacheUser(userId: string, user: User) {
  await redis.set(`user:${userId}`, JSON.stringify(user), {
    ex: 3600, // 1時間
  });
}

export async function getCachedUser(userId: string): Promise<User | null> {
  const cached = await redis.get<string>(`user:${userId}`);
  return cached ? JSON.parse(cached) : null;
}

// または型安全なJSON操作
export async function cacheUserTyped(userId: string, user: User) {
  await redis.set<User>(`user:${userId}`, user, { ex: 3600 });
}

export async function getCachedUserTyped(userId: string) {
  return await redis.get<User>(`user:${userId}`);
}

注意: JSON.stringifyを使う場合、DateオブジェクトはISO文字列に変換されます。再取得時にnew Date()で復元してください。

ハッシュ操作

// セッション管理
export async function setSession(sessionId: string, data: Record<string, string>) {
  await redis.hset(`session:${sessionId}`, data);
  await redis.expire(`session:${sessionId}`, 86400); // 24時間
}

export async function getSession(sessionId: string) {
  return await redis.hgetall<Record<string, string>>(`session:${sessionId}`);
}

export async function updateSessionField(sessionId: string, field: string, value: string) {
  await redis.hset(`session:${sessionId}`, { [field]: value });
}

export async function deleteSession(sessionId: string) {
  await redis.del(`session:${sessionId}`);
}

リスト操作

// 最近のアクティビティログ
export async function addActivityLog(userId: string, activity: string) {
  await redis.lpush(`activity:${userId}`, activity);
  await redis.ltrim(`activity:${userId}`, 0, 99); // 最新100件のみ保持
}

export async function getActivityLogs(userId: string, limit = 10) {
  return await redis.lrange<string>(`activity:${userId}`, 0, limit - 1);
}

セット操作

// タグ管理
export async function addTagToPost(postId: string, tag: string) {
  await redis.sadd(`post:${postId}:tags`, tag);
}

export async function getPostTags(postId: string) {
  return await redis.smembers<string>(`post:${postId}:tags`);
}

export async function removeTagFromPost(postId: string, tag: string) {
  await redis.srem(`post:${postId}:tags`, tag);
}

// 2つの投稿の共通タグ
export async function getCommonTags(postId1: string, postId2: string) {
  return await redis.sinter<string>(`post:${postId1}:tags`, `post:${postId2}:tags`);
}

レート制限

// lib/ratelimit.ts
import { Ratelimit } from '@upstash/ratelimit';
import { redis } from './redis';

// API レート制限(10リクエスト/10秒)
export const apiRatelimit = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(10, '10 s'),
  analytics: true,
});

// ログインレート制限(5試行/1分)
export const loginRatelimit = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(5, '1 m'),
  analytics: true,
});

// グローバルレート制限(1000リクエスト/1時間)
export const globalRatelimit = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(1000, '1 h'),
  analytics: true,
});
// app/api/data/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { apiRatelimit } from '@/lib/ratelimit';

export async function GET(request: NextRequest) {
  const ip = request.ip ?? '127.0.0.1';
  
  const { success, limit, reset, remaining } = await apiRatelimit.limit(ip);

  if (!success) {
    return NextResponse.json(
      { error: 'Rate limit exceeded' },
      {
        status: 429,
        headers: {
          'X-RateLimit-Limit': limit.toString(),
          'X-RateLimit-Remaining': remaining.toString(),
          'X-RateLimit-Reset': reset.toString(),
        },
      }
    );
  }

  // 通常の処理
  return NextResponse.json({ data: 'success' });
}

ポイント: @upstash/ratelimitは、トークンバケット、固定ウィンドウ、スライディングウィンドウの3つのアルゴリズムをサポートします。

ユーザー別レート制限

// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@clerk/nextjs/server';
import { apiRatelimit } from '@/lib/ratelimit';

export async function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith('/api')) {
    const { userId } = auth();
    const identifier = userId ?? request.ip ?? '127.0.0.1';

    const { success } = await apiRatelimit.limit(identifier);

    if (!success) {
      return NextResponse.json(
        { error: 'Rate limit exceeded' },
        { status: 429 }
      );
    }
  }

  return NextResponse.next();
}

export const config = {
  matcher: '/api/:path*',
};

キャッシュ戦略

データベースクエリのキャッシュ

// lib/cache.ts
import { redis } from './redis';
import { db } from './db';

export async function getCachedPost(postId: string) {
  // キャッシュを確認
  const cached = await redis.get<any>(`post:${postId}`);
  
  if (cached) {
    console.log('Cache hit');
    return cached;
  }

  // キャッシュミス時はDBから取得
  console.log('Cache miss');
  const post = await db.post.findUnique({
    where: { id: postId },
    include: { author: true, comments: true },
  });

  if (!post) {
    return null;
  }

  // キャッシュに保存(5分間)
  await redis.set(`post:${postId}`, post, { ex: 300 });

  return post;
}

// 更新時にキャッシュを無効化
export async function updatePost(postId: string, data: any) {
  const updated = await db.post.update({
    where: { id: postId },
    data,
  });

  // キャッシュを削除
  await redis.del(`post:${postId}`);

  return updated;
}

Stale-While-Revalidate

// lib/swr-cache.ts
import { redis } from './redis';

interface CacheEntry<T> {
  data: T;
  timestamp: number;
}

export async function swrCache<T>(
  key: string,
  fetcher: () => Promise<T>,
  ttl: number,
  staleTime: number
): Promise<T> {
  const cached = await redis.get<CacheEntry<T>>(key);

  if (cached) {
    const age = Date.now() - cached.timestamp;

    // フレッシュな場合はそのまま返す
    if (age < staleTime) {
      return cached.data;
    }

    // Staleだが返しつつ、バックグラウンドで更新
    if (age < ttl) {
      // 非同期で再検証
      fetcher().then((fresh) => {
        redis.set(key, { data: fresh, timestamp: Date.now() }, { ex: ttl });
      });

      return cached.data;
    }
  }

  // キャッシュなし、または期限切れの場合は同期取得
  const fresh = await fetcher();
  await redis.set(key, { data: fresh, timestamp: Date.now() }, { ex: ttl });

  return fresh;
}

実践メモ: Stale-While-Revalidateは、古いキャッシュを返しつつバックグラウンドで更新し、レスポンス速度を維持します。

セッション管理

// lib/session.ts
import { redis } from './redis';
import { cookies } from 'next/headers';
import { randomUUID } from 'crypto';

interface SessionData {
  userId: string;
  email: string;
  role: string;
}

export async function createSession(userId: string, email: string, role: string) {
  const sessionId = randomUUID();
  const sessionData: SessionData = { userId, email, role };

  // Redisに保存(7日間)
  await redis.setex(`session:${sessionId}`, 604800, JSON.stringify(sessionData));

  // Cookieに保存
  cookies().set('session', sessionId, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    maxAge: 604800,
  });

  return sessionId;
}

export async function getSession(): Promise<SessionData | null> {
  const sessionId = cookies().get('session')?.value;

  if (!sessionId) {
    return null;
  }

  const sessionData = await redis.get<string>(`session:${sessionId}`);

  if (!sessionData) {
    return null;
  }

  return JSON.parse(sessionData);
}

export async function destroySession() {
  const sessionId = cookies().get('session')?.value;

  if (sessionId) {
    await redis.del(`session:${sessionId}`);
  }

  cookies().delete('session');
}

export async function extendSession() {
  const sessionId = cookies().get('session')?.value;

  if (sessionId) {
    await redis.expire(`session:${sessionId}`, 604800);
  }
}

Pub/Sub

// リアルタイム通知
import { redis } from './redis';

// パブリッシュ
export async function publishNotification(userId: string, message: string) {
  await redis.publish(`user:${userId}:notifications`, message);
}

// サブスクライブ(長時間実行プロセス用)
export async function subscribeToNotifications(userId: string, callback: (message: string) => void) {
  // Note: REST API版では継続的なサブスクリプションは非推奨
  // WebSocketやServer-Sent Eventsと組み合わせる
}

注意: Upstash RedisのREST APIモードでは、SUBSCRIBEは長時間接続に適していません。リアルタイム機能はWebSocketやSSEと組み合わせてください。

パイプライン処理

// 複数操作を一度に実行
export async function incrementMultipleCounters(counters: string[]) {
  const pipeline = redis.pipeline();

  counters.forEach((counter) => {
    pipeline.incr(counter);
  });

  const results = await pipeline.exec();
  return results;
}

// トランザクション的な処理
export async function transferPoints(fromUserId: string, toUserId: string, points: number) {
  const pipeline = redis.pipeline();

  pipeline.decrby(`user:${fromUserId}:points`, points);
  pipeline.incrby(`user:${toUserId}:points`, points);

  const results = await pipeline.exec();
  return results;
}

ポイント: pipelineを使うと、複数コマンドを1回のHTTPリクエストで実行でき、レイテンシを削減できます。

ソート済みセット(ランキング)

// ランキング管理
export async function updateScore(userId: string, score: number) {
  await redis.zadd('leaderboard', { score, member: userId });
}

export async function getTopPlayers(limit = 10) {
  return await redis.zrange<string>('leaderboard', 0, limit - 1, {
    rev: true,
    withScores: true,
  });
}

export async function getUserRank(userId: string) {
  return await redis.zrevrank('leaderboard', userId);
}

export async function getUserScore(userId: string) {
  return await redis.zscore('leaderboard', userId);
}

Geo位置情報

// 位置情報ベースの検索
export async function addLocation(storeId: string, longitude: number, latitude: number) {
  await redis.geoadd('stores', { longitude, latitude, member: storeId });
}

export async function findNearbyStores(longitude: number, latitude: number, radiusKm: number) {
  return await redis.georadius<string>(
    'stores',
    longitude,
    latitude,
    radiusKm,
    'km',
    { withDist: true, withCoord: true, count: 10 }
  );
}

エッジランタイムでの使用

// app/api/edge/route.ts
import { redis } from '@/lib/redis';

export const runtime = 'edge';

export async function GET(request: Request) {
  const url = new URL(request.url);
  const key = url.searchParams.get('key');

  if (!key) {
    return Response.json({ error: 'Missing key' }, { status: 400 });
  }

  const value = await redis.get(key);

  return Response.json({ value });
}

実践メモ: Upstash RedisはHTTPベースなので、edge runtimeでも動作します。

Analytics

// ページビューカウント
export async function trackPageView(slug: string) {
  await redis.incr(`pageviews:${slug}`);
}

export async function getPageViews(slug: string) {
  return (await redis.get<number>(`pageviews:${slug}`)) ?? 0;
}

// 日別統計
export async function trackDailyVisit(date: string, userId: string) {
  await redis.pfadd(`visits:${date}`, userId);
}

export async function getDailyUniqueVisitors(date: string) {
  return await redis.pfcount(`visits:${date}`);
}

関連記事

この技術を体系的に学びたいですか?

未来学では東証プライム上場企業のITエンジニアが24時間サポート。月額24,800円から、退会金0円のオンラインIT塾です。

メールで無料相談する
← 一覧に戻る