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

初級 | 15分 で読める | 2026.04.24

公式ドキュメント

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}`);
}

まとめ

この記事では、手順を追って動かしながら重要な確認ポイントを押さえることが大切です。まずは学習用の最小構成で動作確認し、本番利用する前にセキュリティ、権限、エラー処理、運用手順を見直してください。

参考リソース

  • 公式ドキュメント - Upstash Redis実践 - サーバーレスRedisでデータキャッシング を確認するための一次情報

関連記事

← 一覧に戻る
PR
PR
PR
PR