michangarrito/docs/02-integraciones/INT-013-redis-cache.md
rckrdmrd 2c916e75e5 [SIMCO-V4] feat: Agregar documentación SaaS, ADRs e integraciones
Nuevas Épicas (MCH-029 a MCH-033):
- Infraestructura SaaS multi-tenant
- Auth Social (OAuth2)
- Auditoría Empresarial
- Feature Flags
- Onboarding Wizard

Nuevas Integraciones (INT-010 a INT-014):
- Email Providers (SendGrid, Mailgun, SES)
- Storage Cloud (S3, GCS, Azure)
- OAuth Social
- Redis Cache
- Webhooks Outbound

Nuevos ADRs (0004 a 0011):
- Notifications Realtime
- Feature Flags Strategy
- Storage Abstraction
- Webhook Retry Strategy
- Audit Log Retention
- Rate Limiting
- OAuth Social Implementation
- Email Multi-provider

Actualizados:
- MASTER_INVENTORY.yml
- CONTEXT-MAP.yml
- HERENCIA-SIMCO.md
- Mapas de documentación

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 01:43:15 -06:00

8.4 KiB

id type title provider status integration_type created_at updated_at simco_version tags
INT-013 Integration Redis Cache Redis Planificado infrastructure 2026-01-10 2026-01-10 4.0.1
redis
cache
queue
bullmq
infrastructure

INT-013: Redis Cache

Metadata

Campo Valor
Codigo INT-013
Proveedor Redis
Tipo Infraestructura
Estado Planificado
Multi-tenant Si
Epic Relacionada MCH-029
Owner Backend Team

1. Descripcion

Redis como servicio de cache y queue para mejorar rendimiento y procesar tareas en background. Utilizado para cache de sesiones, configuracion de tenants, y colas de trabajo con BullMQ.

Casos de uso principales:

  • Cache de sesiones JWT
  • Cache de configuracion de tenant
  • Cache de feature flags
  • Queue para emails, webhooks, notificaciones
  • Rate limiting counters
  • Pub/Sub para eventos real-time

2. Credenciales Requeridas

Variables de Entorno

Variable Descripcion Tipo Obligatorio
REDIS_HOST Host del servidor Redis string SI
REDIS_PORT Puerto (default 6379) number SI
REDIS_PASSWORD Password (si auth habilitado) string NO
REDIS_DB Database number (0-15) number NO
REDIS_TLS Usar TLS boolean NO
REDIS_URL URL completa (alternativa) string NO

Ejemplo de .env

# Redis Configuration
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_DB=0

# O usar URL completa
# REDIS_URL=redis://:password@host:6379/0

# Redis Cloud (produccion)
# REDIS_URL=rediss://:password@redis-12345.cloud.redislabs.com:12345

3. Configuracion NestJS

Redis Module

// redis.module.ts
import { Module, Global } from '@nestjs/common';
import { Redis } from 'ioredis';

@Global()
@Module({
  providers: [
    {
      provide: 'REDIS_CLIENT',
      useFactory: () => {
        return new Redis({
          host: process.env.REDIS_HOST,
          port: parseInt(process.env.REDIS_PORT),
          password: process.env.REDIS_PASSWORD,
          db: parseInt(process.env.REDIS_DB || '0'),
        });
      },
    },
  ],
  exports: ['REDIS_CLIENT'],
})
export class RedisModule {}

Cache Service

// cache.service.ts
@Injectable()
export class CacheService {
  constructor(@Inject('REDIS_CLIENT') private readonly redis: Redis) {}

  async get<T>(key: string): Promise<T | null> {
    const data = await this.redis.get(key);
    return data ? JSON.parse(data) : null;
  }

  async set(key: string, value: any, ttlSeconds?: number): Promise<void> {
    const data = JSON.stringify(value);
    if (ttlSeconds) {
      await this.redis.setex(key, ttlSeconds, data);
    } else {
      await this.redis.set(key, data);
    }
  }

  async del(key: string): Promise<void> {
    await this.redis.del(key);
  }

  async invalidatePattern(pattern: string): Promise<void> {
    const keys = await this.redis.keys(pattern);
    if (keys.length > 0) {
      await this.redis.del(...keys);
    }
  }
}

4. BullMQ Queues

Configuracion

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

@Module({
  imports: [
    BullModule.forRoot({
      connection: {
        host: process.env.REDIS_HOST,
        port: parseInt(process.env.REDIS_PORT),
        password: process.env.REDIS_PASSWORD,
      },
    }),
    BullModule.registerQueue(
      { name: 'email' },
      { name: 'webhooks' },
      { name: 'notifications' },
      { name: 'cleanup' },
    ),
  ],
})
export class QueueModule {}

Processor

// email.processor.ts
@Processor('email')
export class EmailProcessor {
  @Process('send')
  async handleSend(job: Job<EmailJobData>) {
    const { to, template, variables } = job.data;
    await this.emailService.send(to, template, variables);
  }
}

Agregar a Queue

// Agregar job
await this.emailQueue.add('send', {
  to: 'user@example.com',
  template: 'welcome',
  variables: { name: 'John' },
}, {
  attempts: 3,
  backoff: {
    type: 'exponential',
    delay: 1000,
  },
});

5. Estructura de Keys

Convenciones de Naming

{prefix}:{scope}:{identifier}:{type}

Ejemplos:
- mch:session:{userId}
- mch:tenant:{tenantId}:config
- mch:tenant:{tenantId}:flags
- mch:rate:{tenantId}:{endpoint}:count
- mch:cache:products:{productId}

Keys por Funcionalidad

Prefijo TTL Descripcion
mch:session:* 24h Sesiones de usuario
mch:tenant:*:config 1h Config de tenant
mch:tenant:*:flags 5m Feature flags
mch:rate:* 1m Rate limiting
mch:cache:* 15m Cache general

6. Rate Limiting

Implementacion Token Bucket

@Injectable()
export class RateLimitService {
  async isAllowed(
    tenantId: string,
    endpoint: string,
    limit: number,
    windowSeconds: number,
  ): Promise<{ allowed: boolean; remaining: number; resetAt: Date }> {
    const key = `mch:rate:${tenantId}:${endpoint}`;
    const now = Date.now();
    const windowStart = now - (windowSeconds * 1000);

    // Remover entradas viejas
    await this.redis.zremrangebyscore(key, 0, windowStart);

    // Contar requests en ventana
    const count = await this.redis.zcard(key);

    if (count >= limit) {
      const oldestEntry = await this.redis.zrange(key, 0, 0, 'WITHSCORES');
      const resetAt = new Date(parseInt(oldestEntry[1]) + (windowSeconds * 1000));
      return { allowed: false, remaining: 0, resetAt };
    }

    // Agregar request actual
    await this.redis.zadd(key, now, `${now}`);
    await this.redis.expire(key, windowSeconds);

    return {
      allowed: true,
      remaining: limit - count - 1,
      resetAt: new Date(now + (windowSeconds * 1000)),
    };
  }
}

7. Multi-tenant

Aislamiento por Prefijo

class TenantCacheService {
  private getKey(tenantId: string, key: string): string {
    return `mch:tenant:${tenantId}:${key}`;
  }

  async getTenantConfig(tenantId: string): Promise<TenantConfig> {
    const key = this.getKey(tenantId, 'config');
    const cached = await this.cache.get<TenantConfig>(key);

    if (cached) return cached;

    const config = await this.db.getTenantConfig(tenantId);
    await this.cache.set(key, config, 3600); // 1 hora
    return config;
  }

  async invalidateTenantCache(tenantId: string): Promise<void> {
    await this.cache.invalidatePattern(`mch:tenant:${tenantId}:*`);
  }
}

8. Health Check

// redis.health.ts
@Injectable()
export class RedisHealthIndicator extends HealthIndicator {
  constructor(@Inject('REDIS_CLIENT') private readonly redis: Redis) {
    super();
  }

  async isHealthy(key: string): Promise<HealthIndicatorResult> {
    try {
      await this.redis.ping();
      return this.getStatus(key, true);
    } catch (error) {
      return this.getStatus(key, false, { error: error.message });
    }
  }
}

Endpoint

@Get('health')
@HealthCheck()
async check() {
  return this.health.check([
    () => this.redis.isHealthy('redis'),
  ]);
}

9. Monitoreo

Metricas

Metrica Descripcion Alerta
redis_connected Conexion activa false
redis_memory_bytes Memoria usada > 80% max
redis_keys_total Total de keys -
redis_cache_hits Cache hits -
redis_cache_misses Cache misses ratio < 80%
redis_queue_length Jobs en queue > 1000

Comandos de Monitoreo

# Info general
redis-cli INFO

# Memory
redis-cli INFO memory

# Keys por patron
redis-cli KEYS "mch:tenant:*" | wc -l

# Queue length
redis-cli LLEN bull:email:wait

10. Testing

Docker Compose

services:
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data

Mock para Tests

// test/mocks/redis.mock.ts
export const mockRedis = {
  get: jest.fn(),
  set: jest.fn(),
  setex: jest.fn(),
  del: jest.fn(),
  keys: jest.fn().mockResolvedValue([]),
  ping: jest.fn().mockResolvedValue('PONG'),
};

11. Referencias


Ultima actualizacion: 2026-01-10 Autor: Backend Team