docs//[object Object]

[object Object]

High-Speed Redis Cache Patterns für Discord Bots: Sub-Millisekunden Leaderboards

Wenn ein Discord-Bot auf Tausenden von Servern aktiv ist, führt jede Chat-Nachricht (XP-Vergabe, Level-Aufstiege, Cooldown-Prüfungen) zu Datenbank-Interaktionen. Würde jede Nachricht einen SQL-Befehl auf PostgreSQL absetzen (SELECT * FROM users WHERE id = ?), bricht der Datenbank-Verbindungspool bei hoher Aktivität schnell zusammen.

Mit Redis als In-Memory-Caching-Layer lagern wir transiente Zustände in den RAM aus. Abfragen laufen in unter 1 Millisekunde ab und entlasten die persistente SQL-Datenbank um bis zu 95 %.


1. Die Caching-Topologie: Write-Through vs. Write-Behind

┌─────────────────────────────────────────────────────────────────┐
│                     Discord Gateway Events                      │
└────────────────────────────────┬────────────────────────────────┘
                                 │
                                 ▼
┌─────────────────────────────────────────────────────────────────┐
│                        KiroCloud Bot Engine                     │
├────────────────────────────────┬────────────────────────────────┤
│ 1. Schnelle In-Memory Abfrage  │ 2. Asynchrone Batch-Speicherung│
│    (Latenz: 0.4 ms)            │    (alle 60 Sekunden)          │
│    ▼                           │    ▼                           │
│  ┌───────────────────────┐     │  ┌──────────────────────────┐  │
│  │     Redis Cache       │     │  │  PostgreSQL Datenbank    │  │
│  │ (Sorted Sets, TTL)    │     │  │  (Persistente Sicherung) │  │
│  └───────────────────────┘     │  └──────────────────────────┘  │
└────────────────────────────────┴────────────────────────────────┘

2. Echtzeit-Ranglisten mit Redis Sorted Sets (ZSET)

Der naheliegendste Anwendungsfall für Redis in Discord-Bots sind Level- und Coin-Ranglisten (/leaderboard). In SQL erfordert dies ein teures ORDER BY xp DESC LIMIT 10, das bei Tabellen mit Millionen Zeilen erhebliche CPU-Zeit beansprucht.

In Redis nutzen wir Sorted Sets (ZADD, ZREVRANGE, ZREVRANK), die intern als Skip-Lists organisiert sind und Rangabfragen in $O(\log N)$ Komplexität lösen:

import Redis from 'ioredis';

const redis = new Redis(process.env.REDIS_URL || 'redis://localhost:6379');

/**
 * Erhöht die Erfahrungspunkte eines Nutzers auf einem Server
 */
export async function addGuildUserXp(guildId: string, userId: string, xpDelta: number): Promise<number> {
  const leaderboardKey = `guild:${guildId}:xp_leaderboard`;

  // ZINCRBY erhöht den Score atomar und liefert den neuen Wert zurück
  const newXp = await redis.zincrby(leaderboardKey, xpDelta, userId);
  return parseFloat(newXp);
}

/**
 * Ermittelt die Top 10 Nutzer eines Servers in Sub-Millisekunden
 */
export async function getTopUsers(guildId: string, limit = 10): Promise<Array<{ userId: string; score: number }>> {
  const leaderboardKey = `guild:${guildId}:xp_leaderboard`;

  // ZREVRANGE mit WITHSCORES liefert die ranghöchsten Einträge
  const rawResults = await redis.zrevrange(leaderboardKey, 0, limit - 1, 'WITHSCORES');

  const formatted: Array<{ userId: string; score: number }> = [];
  for (let i = 0; i < rawResults.length; i += 2) {
    formatted.push({
      userId: rawResults[i],
      score: parseFloat(rawResults[i + 1]),
    });
  }

  return formatted;
}

/**
 * Ermittelt den genauen Platz (Rank) eines bestimmten Nutzers
 */
export async function getUserRank(guildId: string, userId: string): Promise<number | null> {
  const leaderboardKey = `guild:${guildId}:xp_leaderboard`;
  // ZREVRANK liefert den 0-basierten Index absteigend sortiert
  const rank = await redis.zrevrank(leaderboardKey, userId);
  return rank !== null ? rank + 1 : null;
}

3. Cooldown-Verwaltung mit automatischer TTL-Verfallszeit

Befehls-Cooldowns (z. B. 5 Sekunden Pause zwischen /daily) erfordern keine persistente Speicherung. Mit Redis-TTL verfällt der Schlüssel automatisch:

export async function checkRateLimit(userId: string, action: string, windowSeconds: number): Promise<{ allowed: boolean; remainingSec: number }> {
  const key = `ratelimit:${userId}:${action}`;

  // SET mit NX (nur setzen wenn nicht existiert) und EX (Ablaufzeit)
  const acquired = await redis.set(key, '1', 'EX', windowSeconds, 'NX');

  if (acquired === 'OK') {
    return { allowed: true, remainingSec: 0 };
  }

  const ttl = await redis.ttl(key);
  return { allowed: false, remainingSec: Math.max(0, ttl) };
}

4. Cache-Invalidierung & PostgreSQL-Synchronisation

Um Datenverlust bei Server-Neustarts zu verhindern, synchronisieren wir geänderte Redis-Scores periodisch in die relationale PostgreSQL-Datenbank (Write-Behind-Pattern). Dies geschieht im Hintergrund alle 60 Sekunden ohne Blockierung des Discord-Event-Loops.

Luca Lichtenwald
Artikel verfasst vonGründer & Lead Cloud Architect

Luca Lichtenwald

Spezialisiert auf hochverfügbare Discord-Bot-Infrastrukturen, V8-Memory-Tuning und kryptografische Zero-Trust-Architekturen.

Cookie-Einstellungen

Wir verwenden Cookies, um deine Erfahrung zu verbessern und unsere Dienste optimal anzubieten. Du kannst deine Einstellungen jederzeit anpassen.

main/11f725c