Upstash Redis 統合設計書¶
概要¶
Vendure における Upstash Redis の統合設計および設定方法について説明します。Redis は以下の用途で使用されます:
- セッション管理 - ユーザー認証セッションの保存
- BullMQ ジョブキュー - バックグラウンドタスクの処理
- キャッシュ戦略 - GraphQL クエリ結果や商品データのキャッシュ
- レート制限 - API 使用制限の管理
Upstash Redis セットアップ¶
1. Redis インスタンス作成¶
# Fly.io CLI で Upstash Redis を作成
flyctl redis create
# 選択項目:
# Organization: ritsubi
# Redis database name: ritsubi-vendure-redis
# Primary region: Tokyo (nrt)
# Eviction policy: noeviction (BullMQ 用に重要)
# Plan: Fixed $3 plan (3GB Max Data Size 推奨)
2. Redis 接続情報取得¶
# Redis接続情報を確認
flyctl redis status ritsubi-vendure-redis
# 接続情報をシークレットに設定
flyctl secrets set UPSTASH_REDIS_URL="redis://..." -a ritsubi-vendure
flyctl secrets set UPSTASH_REDIS_PASSWORD="..." -a ritsubi-vendure
Redis 接続設定¶
基本接続設定¶
// redis-config.ts
import { Redis } from 'ioredis';
export interface RedisConfig {
host: string;
port: number;
password: string;
keyPrefix?: string;
ttl?: number;
tls?: {
rejectUnauthorized: boolean;
};
}
export const createRedisConnection = (config: RedisConfig): Redis => {
return new Redis({
host: config.host,
port: config.port,
password: config.password,
keyPrefix: config.keyPrefix,
tls: config.tls || { rejectUnauthorized: false },
retryDelayOnFailover: 1000,
maxRetriesPerRequest: 3,
lazyConnect: true,
keepAlive: 30000,
family: 4, // IPv4
connectTimeout: 10000,
commandTimeout: 5000,
});
};
環境別設定¶
// redis-configs.ts
export const redisConfigs = {
development: {
host: 'localhost',
port: 6379,
password: '',
keyPrefix: 'dev:',
},
staging: {
host: process.env.UPSTASH_REDIS_HOST_STAGING!,
port: 6379,
password: process.env.UPSTASH_REDIS_PASSWORD_STAGING!,
keyPrefix: 'staging:',
tls: { rejectUnauthorized: false },
},
production: {
host: process.env.UPSTASH_REDIS_HOST!,
port: 6379,
password: process.env.UPSTASH_REDIS_PASSWORD!,
keyPrefix: 'prod:',
tls: { rejectUnauthorized: false },
},
};
export const getRedisConfig = (
env: string = process.env.NODE_ENV || 'development',
) => {
return redisConfigs[env as keyof typeof redisConfigs];
};
セッション管理設定¶
RedisSessionCacheStrategy 実装¶
// redis-session-cache-strategy.ts
import { CachedSession, Logger, SessionCacheStrategy } from '@vendure/core';
import { Redis } from 'ioredis';
export class RedisSessionCacheStrategy implements SessionCacheStrategy {
private readonly logger = new Logger(RedisSessionCacheStrategy.name);
private redis: Redis;
constructor(
private config: {
redisOptions: RedisConfig;
ttl?: number; // セッション有効期限(秒)
},
) {
this.redis = createRedisConnection(config.redisOptions);
}
async get(sessionId: string): Promise<CachedSession | undefined> {
try {
const key = this.getSessionKey(sessionId);
const data = await this.redis.get(key);
if (!data) {
return undefined;
}
return JSON.parse(data) as CachedSession;
} catch (error) {
this.logger.error(`Failed to get session ${sessionId}:`, error);
return undefined;
}
}
async set(session: CachedSession): Promise<void> {
try {
const key = this.getSessionKey(session.id);
const data = JSON.stringify(session);
const ttl = this.config.ttl || 7 * 24 * 60 * 60; // 7日間デフォルト
await this.redis.setex(key, ttl, data);
} catch (error) {
this.logger.error(`Failed to set session ${session.id}:`, error);
throw error;
}
}
async delete(sessionId: string): Promise<void> {
try {
const key = this.getSessionKey(sessionId);
await this.redis.del(key);
} catch (error) {
this.logger.error(`Failed to delete session ${sessionId}:`, error);
throw error;
}
}
async clear(): Promise<void> {
try {
const pattern = this.getSessionKey('*');
const keys = await this.redis.keys(pattern);
if (keys.length > 0) {
await this.redis.del(...keys);
}
} catch (error) {
this.logger.error('Failed to clear sessions:', error);
throw error;
}
}
private getSessionKey(sessionId: string): string {
return `session:${sessionId}`;
}
}
BullMQ ジョブキュー設定¶
BullMQ Redis 設定¶
// bullmq-redis-strategy.ts
import { BullMQJobQueueStrategy } from '@vendure/job-queue-plugin/package/bullmq';
import { ConnectionOptions } from 'bullmq';
export const createBullMQConfig = (): ConnectionOptions => {
return {
host: process.env.UPSTASH_REDIS_HOST!,
port: 6379,
password: process.env.UPSTASH_REDIS_PASSWORD!,
tls: { rejectUnauthorized: false },
// BullMQ 固有設定
maxRetriesPerRequest: null, // 重要: null に設定
retryDelayOnFailover: 1000,
enableReadyCheck: false,
maxRetriesPerRequest: null,
// 接続プール設定
lazyConnect: true,
keepAlive: 30000,
family: 4,
};
};
export const bullMQJobQueueStrategy = new BullMQJobQueueStrategy({
connection: createBullMQConfig(),
});
ジョブキュー設定¶
// job-queue-config.ts
export const jobQueueConfig = {
// デフォルト設定
defaultJobOptions: {
removeOnComplete: 10, // 完了ジョブを10件まで保持
removeOnFail: 50, // 失敗ジョブを50件まで保持
attempts: 3, // 最大3回リトライ
backoff: {
type: 'exponential',
delay: 2000,
},
},
// 各ジョブタイプ別設定
jobTypes: {
'send-email': {
attempts: 5,
backoff: {
type: 'exponential',
delay: 1000,
},
},
'monthly-rebate-calculation': {
attempts: 1, // リトライなし(重要な計算)
removeOnComplete: 100, // 履歴を多く保持
},
'inventory-sync': {
attempts: 3,
delay: 60000, // 1分遅延
repeat: {
pattern: '0 */6 * * *', // 6時間おき
},
},
'image-processing': {
attempts: 2,
backoff: {
type: 'fixed',
delay: 5000,
},
},
},
};
キャッシュ戦略設定¶
Redis キャッシュ戦略実装¶
// redis-cache-strategy.ts
import { CacheStrategy, Logger } from '@vendure/core';
import { Redis } from 'ioredis';
export class RedisCacheStrategy implements CacheStrategy {
private readonly logger = new Logger(RedisCacheStrategy.name);
private redis: Redis;
constructor(
private config: {
redisOptions: RedisConfig;
defaultTtl?: number;
},
) {
this.redis = createRedisConnection({
...config.redisOptions,
keyPrefix: 'cache:',
});
}
async get<T = any>(key: string): Promise<T | undefined> {
try {
const data = await this.redis.get(key);
return data ? JSON.parse(data) : undefined;
} catch (error) {
this.logger.error(`Cache get failed for key ${key}:`, error);
return undefined;
}
}
async set<T = any>(key: string, value: T, ttl?: number): Promise<void> {
try {
const data = JSON.stringify(value);
const expiry = ttl || this.config.defaultTtl || 3600; // 1時間デフォルト
await this.redis.setex(key, expiry, data);
} catch (error) {
this.logger.error(`Cache set failed for key ${key}:`, error);
}
}
async delete(key: string): Promise<void> {
try {
await this.redis.del(key);
} catch (error) {
this.logger.error(`Cache delete failed for key ${key}:`, error);
}
}
async invalidateMany(pattern: string): Promise<void> {
try {
const keys = await this.redis.keys(pattern);
if (keys.length > 0) {
await this.redis.del(...keys);
}
} catch (error) {
this.logger.error(
`Cache invalidation failed for pattern ${pattern}:`,
error,
);
}
}
async has(key: string): Promise<boolean> {
try {
const exists = await this.redis.exists(key);
return exists === 1;
} catch (error) {
this.logger.error(`Cache exists check failed for key ${key}:`, error);
return false;
}
}
}
キャッシュ使用例¶
// product-cache-service.ts
export class ProductCacheService {
constructor(private cacheStrategy: RedisCacheStrategy) {}
async getProductDetails(productId: string) {
const cacheKey = `product:details:${productId}`;
const cached = await this.cacheStrategy.get(cacheKey);
if (cached) {
return cached;
}
// データベースから取得
const product = await this.productService.findOne(productId);
// 1時間キャッシュ
await this.cacheStrategy.set(cacheKey, product, 3600);
return product;
}
async invalidateProductCache(productId: string) {
await this.cacheStrategy.invalidateMany(`product:*:${productId}`);
}
}
レート制限設定¶
Redis ベースレート制限¶
// redis-rate-limiter.ts
export class RedisRateLimiter {
constructor(private redis: Redis) {}
async checkLimit(
key: string,
limit: number,
windowMs: number,
): Promise<{ allowed: boolean; remaining: number; resetTime: Date }> {
const window = Math.floor(Date.now() / windowMs);
const redisKey = `rate_limit:${key}:${window}`;
try {
const current = await this.redis.incr(redisKey);
if (current === 1) {
// 新しいウィンドウの場合、TTLを設定
await this.redis.expire(redisKey, Math.ceil(windowMs / 1000));
}
const remaining = Math.max(0, limit - current);
const resetTime = new Date((window + 1) * windowMs);
return {
allowed: current <= limit,
remaining,
resetTime,
};
} catch (error) {
// Redis エラーの場合は制限を無効化
return {
allowed: true,
remaining: limit,
resetTime: new Date(Date.now() + windowMs),
};
}
}
}
// 使用例
export const apiRateLimiter = new RedisRateLimiter(redisConnection);
// GraphQL API レート制限ミドルウェア
export const rateLimitMiddleware = async (
req: Request,
res: Response,
next: NextFunction,
) => {
const clientId = req.ip || 'unknown';
const result = await apiRateLimiter.checkLimit(
`api:${clientId}`,
100, // 1分間に100リクエスト
60 * 1000, // 1分間
);
if (!result.allowed) {
return res.status(429).json({
error: 'Rate limit exceeded',
resetTime: result.resetTime,
});
}
next();
};
Vendure 設定統合¶
完全な Vendure 設定¶
// vendure-config.ts
import { VendureConfig } from '@vendure/core';
import { BullMQJobQueuePlugin } from '@vendure/job-queue-plugin/package/bullmq';
export const config: VendureConfig = {
// Redis セッション管理
authOptions: {
sessionCacheStrategy: new RedisSessionCacheStrategy({
redisOptions: getRedisConfig(),
ttl: 7 * 24 * 60 * 60, // 7日間
}),
},
// Redis キャッシュ戦略
systemOptions: {
cacheStrategy: new RedisCacheStrategy({
redisOptions: getRedisConfig(),
defaultTtl: 3600, // 1時間
}),
},
// BullMQ ジョブキュー
plugins: [
BullMQJobQueuePlugin.init({
connection: createBullMQConfig(),
...jobQueueConfig,
}),
],
};
監視とメトリクス¶
Redis 監視設定¶
// redis-metrics.ts
export class RedisMetrics {
constructor(private redis: Redis) {}
async getConnectionStats() {
const info = await this.redis.info('server');
const stats = await this.redis.info('stats');
return {
uptime: this.parseInfo(info, 'uptime_in_seconds'),
connectedClients: this.parseInfo(stats, 'connected_clients'),
usedMemory: this.parseInfo(info, 'used_memory'),
keyspace: await this.getKeyspaceInfo(),
};
}
async getKeyspaceInfo() {
const keyspace = await this.redis.info('keyspace');
return {
totalKeys: this.parseInfo(keyspace, 'keys'),
expires: this.parseInfo(keyspace, 'expires'),
};
}
private parseInfo(info: string, key: string): number {
const match = info.match(new RegExp(`${key}:(\\d+)`));
return match ? parseInt(match[1], 10) : 0;
}
}
// ヘルスチェック
export const redisHealthCheck = async (redis: Redis): Promise<boolean> => {
try {
const result = await redis.ping();
return result === 'PONG';
} catch {
return false;
}
};
トラブルシューティング¶
一般的な問題と解決方法¶
1. 接続タイムアウト¶
// 接続設定の調整
const redisConfig = {
connectTimeout: 10000, // 10秒
commandTimeout: 5000, // 5秒
retryDelayOnFailover: 1000,
maxRetriesPerRequest: 3,
};
2. BullMQ ジョブ失敗¶
# Redis eviction policy を確認
redis-cli CONFIG GET maxmemory-policy
# 結果: noeviction である必要がある
# Upstash コンソールで確認・設定
3. メモリ使用量の最適化¶
// TTL の適切な設定
const cacheConfig = {
shortTerm: 300, // 5分(商品価格など)
mediumTerm: 3600, // 1時間(商品詳細など)
longTerm: 86400, // 24時間(カテゴリなど)
};
// 不要なキーの削除
await redis.del('temporary:*');
4. パフォーマンス最適化¶
// Pipeline の使用
const pipeline = redis.pipeline();
pipeline.set('key1', 'value1');
pipeline.set('key2', 'value2');
pipeline.set('key3', 'value3');
await pipeline.exec();
// Lua スクリプトの活用
const script = `
local keys = redis.call('keys', ARGV[1])
for i=1,#keys do
redis.call('del', keys[i])
end
return #keys
`;
const deletedCount = await redis.eval(script, 0, 'cache:product:*');
文書バージョン: 1.0 作成日: 2025年9月17日 次回レビュー: 2025年12月17日