Skip to content

Redis

O Redis é utilizado para múltiplos propósitos no CSGOFlip: sessions, cache, filas de processamento e pub/sub para WebSocket.

Casos de Uso

Configuração

Conexão

typescript
// redis.config.ts
import { RedisOptions } from 'ioredis';

export const redisConfig: RedisOptions = {
  host: process.env.REDIS_HOST || 'localhost',
  port: parseInt(process.env.REDIS_PORT || '6379'),
  password: process.env.REDIS_PASSWORD,
  db: 0,
  
  // Retry
  retryStrategy: (times: number) => {
    if (times > 10) {
      return null; // Desistir
    }
    return Math.min(times * 50, 2000);
  },
  
  // Timeouts
  connectTimeout: 10000,
  commandTimeout: 5000,
  
  // Keep alive
  keepAlive: 30000,
  
  // TLS (produção)
  tls: process.env.NODE_ENV === 'production' ? {} : undefined,
};

Serviço Redis

typescript
// redis.service.ts
@Injectable()
export class RedisService implements OnModuleInit, OnModuleDestroy {
  private client: Redis;
  
  constructor() {
    this.client = new Redis(redisConfig);
  }
  
  async onModuleInit() {
    this.client.on('error', (err) => {
      console.error('Redis error:', err);
    });
    
    this.client.on('connect', () => {
      console.log('Redis connected');
    });
  }
  
  async onModuleDestroy() {
    await this.client.quit();
  }
  
  // Operações básicas
  async get(key: string): Promise<string | null> {
    return this.client.get(key);
  }
  
  async set(key: string, value: string, ttl?: number): Promise<void> {
    if (ttl) {
      await this.client.setex(key, ttl, value);
    } else {
      await this.client.set(key, value);
    }
  }
  
  async del(key: string): Promise<void> {
    await this.client.del(key);
  }
  
  async exists(key: string): Promise<boolean> {
    return (await this.client.exists(key)) === 1;
  }
  
  // Operações de lista
  async lpush(key: string, value: string): Promise<void> {
    await this.client.lpush(key, value);
  }
  
  async lrange(key: string, start: number, stop: number): Promise<string[]> {
    return this.client.lrange(key, start, stop);
  }
  
  async ltrim(key: string, start: number, stop: number): Promise<void> {
    await this.client.ltrim(key, start, stop);
  }
  
  // Operações de set
  async sadd(key: string, member: string): Promise<void> {
    await this.client.sadd(key, member);
  }
  
  async srem(key: string, member: string): Promise<void> {
    await this.client.srem(key, member);
  }
  
  async smembers(key: string): Promise<string[]> {
    return this.client.smembers(key);
  }
  
  // Operações de hash
  async hset(key: string, field: string, value: string): Promise<void> {
    await this.client.hset(key, field, value);
  }
  
  async hget(key: string, field: string): Promise<string | null> {
    return this.client.hget(key, field);
  }
  
  async hgetall(key: string): Promise<Record<string, string>> {
    return this.client.hgetall(key);
  }
  
  // Incremento atômico
  async incr(key: string): Promise<number> {
    return this.client.incr(key);
  }
  
  async decr(key: string): Promise<number> {
    return this.client.decr(key);
  }
  
  // TTL
  async expire(key: string, seconds: number): Promise<void> {
    await this.client.expire(key, seconds);
  }
  
  async ttl(key: string): Promise<number> {
    return this.client.ttl(key);
  }
  
  // Pub/Sub
  async publish(channel: string, message: string): Promise<void> {
    await this.client.publish(channel, message);
  }
  
  subscribe(channel: string, callback: (message: string) => void): void {
    const subscriber = this.client.duplicate();
    subscriber.subscribe(channel);
    subscriber.on('message', (ch, message) => {
      if (ch === channel) {
        callback(message);
      }
    });
  }
}

Sessions

typescript
// session.service.ts
@Injectable()
export class SessionService {
  private readonly PREFIX = 'session:';
  private readonly TTL = 7 * 24 * 60 * 60; // 7 dias
  
  constructor(private redis: RedisService) {}
  
  async create(userId: bigint, metadata: SessionMetadata): Promise<string> {
    const sessionId = crypto.randomUUID();
    const key = `${this.PREFIX}${sessionId}`;
    
    const session: Session = {
      userId: userId.toString(),
      createdAt: new Date().toISOString(),
      expiresAt: new Date(Date.now() + this.TTL * 1000).toISOString(),
      ...metadata,
    };
    
    await this.redis.set(key, JSON.stringify(session), this.TTL);
    
    // Adicionar à lista de sessões do usuário
    await this.redis.sadd(`user:${userId}:sessions`, sessionId);
    
    return sessionId;
  }
  
  async validate(sessionId: string): Promise<Session | null> {
    const key = `${this.PREFIX}${sessionId}`;
    const data = await this.redis.get(key);
    
    if (!data) return null;
    
    const session = JSON.parse(data);
    
    // Atualizar última atividade
    session.lastActivity = new Date().toISOString();
    await this.redis.set(key, JSON.stringify(session), this.TTL);
    
    return session;
  }
  
  async invalidate(sessionId: string): Promise<void> {
    const key = `${this.PREFIX}${sessionId}`;
    const data = await this.redis.get(key);
    
    if (data) {
      const session = JSON.parse(data);
      await this.redis.srem(`user:${session.userId}:sessions`, sessionId);
    }
    
    await this.redis.del(key);
  }
  
  async invalidateAllUserSessions(userId: bigint): Promise<void> {
    const sessions = await this.redis.smembers(`user:${userId}:sessions`);
    
    for (const sessionId of sessions) {
      await this.redis.del(`${this.PREFIX}${sessionId}`);
    }
    
    await this.redis.del(`user:${userId}:sessions`);
  }
}

Cache

typescript
// cache.service.ts
@Injectable()
export class CacheService {
  constructor(private redis: RedisService) {}
  
  async get<T>(key: string): Promise<T | null> {
    const data = await this.redis.get(`cache:${key}`);
    return data ? JSON.parse(data) : null;
  }
  
  async set<T>(key: string, value: T, ttl: number = 300): Promise<void> {
    await this.redis.set(`cache:${key}`, JSON.stringify(value), ttl);
  }
  
  async invalidate(key: string): Promise<void> {
    await this.redis.del(`cache:${key}`);
  }
  
  async invalidatePattern(pattern: string): Promise<void> {
    const keys = await this.redis.keys(`cache:${pattern}`);
    for (const key of keys) {
      await this.redis.del(key);
    }
  }
  
  // Cache decorator helper
  async cached<T>(
    key: string,
    ttl: number,
    factory: () => Promise<T>,
  ): Promise<T> {
    const cached = await this.get<T>(key);
    
    if (cached !== null) {
      return cached;
    }
    
    const value = await factory();
    await this.set(key, value, ttl);
    
    return value;
  }
}

// Exemplo de uso
async getCases(): Promise<Case[]> {
  return this.cacheService.cached(
    'cases:active',
    300, // 5 minutos
    () => this.caseRepository.findActive(),
  );
}

BullMQ Queues

typescript
// queue.module.ts
import { BullModule } from '@nestjs/bullmq';

@Module({
  imports: [
    BullModule.forRoot({
      connection: {
        host: process.env.REDIS_HOST,
        port: parseInt(process.env.REDIS_PORT || '6379'),
        password: process.env.REDIS_PASSWORD,
      },
      defaultJobOptions: {
        attempts: 3,
        backoff: {
          type: 'exponential',
          delay: 1000,
        },
        removeOnComplete: 100,
        removeOnFail: 1000,
      },
    }),
    BullModule.registerQueue(
      { name: 'payment' },
      { name: 'notification' },
      { name: 'email' },
    ),
  ],
})
export class QueueModule {}

Processor

typescript
// payment.processor.ts
@Processor('payment')
export class PaymentProcessor {
  constructor(
    private paymentService: PaymentService,
    private logger: Logger,
  ) {}
  
  @Process('process-withdrawal')
  async processWithdrawal(job: Job<{ withdrawalId: string }>) {
    this.logger.log(`Processing withdrawal ${job.data.withdrawalId}`);
    
    try {
      await this.paymentService.processWithdrawal(job.data.withdrawalId);
      return { success: true };
    } catch (error) {
      this.logger.error(`Withdrawal failed: ${error.message}`);
      throw error; // Retry
    }
  }
  
  @Process('process-deposit')
  async processDeposit(job: Job<{ depositId: string }>) {
    await this.paymentService.confirmDeposit(job.data.depositId);
    return { success: true };
  }
}

Adicionar Job

typescript
// payment.service.ts
@Injectable()
export class PaymentService {
  constructor(
    @InjectQueue('payment') private paymentQueue: Queue,
  ) {}
  
  async scheduleWithdrawalProcessing(withdrawalId: string): Promise<void> {
    await this.paymentQueue.add(
      'process-withdrawal',
      { withdrawalId },
      {
        delay: 5000, // 5s delay
        priority: 1,
      },
    );
  }
}

Distributed Locks (Redlock)

typescript
// redlock.service.ts
import Redlock from 'redlock';

@Injectable()
export class RedlockService implements OnModuleInit {
  private redlock: Redlock;
  
  constructor(private redis: RedisService) {}
  
  onModuleInit() {
    this.redlock = new Redlock([this.redis.client], {
      driftFactor: 0.01,
      retryCount: 10,
      retryDelay: 200,
      retryJitter: 200,
      automaticExtensionThreshold: 500,
    });
    
    this.redlock.on('error', (error) => {
      console.error('Redlock error:', error);
    });
  }
  
  async using<T>(
    resources: string[],
    duration: number,
    routine: () => Promise<T>,
  ): Promise<T> {
    return this.redlock.using(resources, duration, routine);
  }
  
  async acquire(
    resources: string[],
    duration: number,
  ): Promise<Lock> {
    return this.redlock.acquire(resources, duration);
  }
}

// Exemplo de uso
async openCase(userId: bigint, caseId: bigint) {
  const lockKey = `user:${userId}:case-open`;
  
  return this.redlock.using([lockKey], 30000, async () => {
    // Operação protegida por lock
    const balance = await this.getBalance(userId);
    // ...
  });
}

Rate Limiting

typescript
// rate-limit.service.ts
@Injectable()
export class RateLimitService {
  constructor(private redis: RedisService) {}
  
  async isRateLimited(
    key: string,
    limit: number,
    windowSeconds: number,
  ): Promise<boolean> {
    const redisKey = `ratelimit:${key}`;
    
    const current = await this.redis.incr(redisKey);
    
    if (current === 1) {
      await this.redis.expire(redisKey, windowSeconds);
    }
    
    return current > limit;
  }
  
  async getRemainingAttempts(
    key: string,
    limit: number,
  ): Promise<number> {
    const redisKey = `ratelimit:${key}`;
    const current = await this.redis.get(redisKey);
    
    return Math.max(0, limit - parseInt(current || '0', 10));
  }
}

Monitoramento

Métricas

bash
# Info geral
redis-cli INFO

# Memória
redis-cli INFO memory

# Clientes
redis-cli CLIENT LIST

# Slow log
redis-cli SLOWLOG GET 10

# Keys por pattern
redis-cli KEYS "session:*" | wc -l

Dashboard

typescript
// redis-monitor.service.ts
async getStats(): Promise<RedisStats> {
  const info = await this.redis.info();
  
  return {
    connectedClients: info.connected_clients,
    usedMemory: info.used_memory_human,
    totalKeys: info.db0?.keys || 0,
    hitRate: info.keyspace_hits / (info.keyspace_hits + info.keyspace_misses),
    uptime: info.uptime_in_days,
  };
}

Configuração de Produção

conf
# redis.conf

# Memória
maxmemory 2gb
maxmemory-policy allkeys-lru

# Persistência
appendonly yes
appendfsync everysec

# Segurança
requirepass your-strong-password
bind 127.0.0.1

# Performance
tcp-keepalive 300
timeout 0

# Logs
loglevel notice
logfile /var/log/redis/redis.log

Documentação Técnica CSGOFlip