Redis Wiederverbindungsstrategie in NestJS
Die Zwischenspeicherung von Daten ist eine entscheidende Strategie zur Verbesserung der Leistung unserer NestJS-Anwendung. Sie ermöglicht es uns, Informationen von Anfragen an externe APIs zu speichern, insbesondere wenn diese Anfragen tendenziell identische Antworten liefern.
Betrachten wir folgendes Szenario: Unsere erste Anfrage an eine externe API benötigt 1537 ms für die Antwort (siehe Abb. 1). Da sich der Antwortinhalt jedoch nicht ändert, profitieren nachfolgende Anfragen von den zwischengespeicherten Daten, was die Antwortzeit auf nur 38 ms reduziert (siehe Abb. 2).
Dieser Artikel beschäftigt sich mit der komplexen Redis-Reconnect-Strategie innerhalb von NestJS und erklärt, wie ein solcher Mechanismus die Zwischenspeicherungsverfahren optimiert und die Effizienz unserer Anwendung erhöht. Begleite uns, während wir die Feinheiten und Implementierungstechniken dieser entscheidenden Strategie erkunden.


Wie konfiguriert man Redis in NestJS?
Die Konfiguration von Redis in NestJS beinhaltet das Importieren des CacheModule zusammen mit dem redisStore, der von den NPM-Paketen @nestjs/cache-manager und cache-manager-redis-store bereitgestellt wird.
@Module({
imports: [
CacheModule.registerAsync<RedisClientOptions>({
imports: [ConfigModule],
isGlobal: true,
useFactory: async (config: ConfigService) => {
const store = await redisStore({
socket: {
host: config.get<string>('REDIS_HOST'),
port: config.get<number>('REDIS_PORT'),
},
password: config.get<string>('REDIS_PASSWORD'),
});
return {
store: store,
} as CacheModuleAsyncOptions;
},
inject: [ConfigService],
}),
],
})
export class CacheConfigModule {}
Was passiert, wenn meine App die Verbindung zu Redis verliert?
IIm unglücklichen Fall, dass unser Server die Verbindung zu Redis verliert, kommt unsere Anwendung zum Stillstand wie im folgenden Bild zu sehen ist.

Um dieses Problem zu umgehen, ist die Implementierung einer robusten Reconnect-Strategie unerlässlich. Diese Strategie ermöglicht es unserem Server, automatisch erneute Verbindungsversuche durchzuführen, sobald Redis wieder verfügbar ist.
Im folgenden Beispiel ist unsere Anwendung so konfiguriert, dass sie alle 3 Sekunden einen erneuten Verbindungsversuch unternimmt:
useFactory: async (config: ConfigService) => {
const store = await redisStore({
socket: {
host: config.get<string>('REDIS_HOST'),
port: config.get<number>('REDIS_PORT'),
reconnectStrategy: (retries) => {
console.log(`redis reconnect attempt: ${retries}`);
return 3000;
},
},
password: config.get<string>('REDIS_PASSWORD'),
});
return {
store: store,
} as CacheModuleAsyncOptions;
},
Dennoch stoßen wir auf ein unerwartetes Verhalten, wenn wir versuchen den Redis-Server herunterzufahren während unser NestJS-App-Server weiterhin aktiv ist.
Beim Durchsehen der Projektprotokolle zeigt sich ein seltsames Verhalten: Die Reconnect-Strategie unternimmt nur einen Verbindungsversuch und stellt dann alle weiteren Versuche ein. Aber warum geschieht das?
Umgang mit Redis-Verbindungsabbrüchen in NestJS
Um dieses Problem anzugehen, müssen wir zunächst verstehen, wie NestJS mit Redis-Verbindungsabbrüchen umgeht. Dazu sind einige Änderungen erforderlich, um Zugriff auf den Redis-Client zu erhalten und um auf auftretende Fehler zu hören, indem wir das Fehlerereignis abonnieren.
useFactory: async (config: ConfigService) => {
const store = await redisStore({
socket: {
host: config.get<string>('REDIS_HOST'),
port: config.get<number>('REDIS_PORT'),
reconnectStrategy: (retries) => {
console.log(`redis reconnect attempt: ${retries}`);
return 3000;
},
},
password: config.get<string>('REDIS_PASSWORD'),
});
return {
store: store,
} as CacheModuleAsyncOptions;
},
Nach dem erneuten Ausführen des Tests und dem Trennen des Redis-Dienstes während des Betriebs unserer App stellen wir fest, dass der Fehler sich wie folgt äußert:

Und wenn wir den Redis-Dienst neu starten, versucht unsere App nicht, neue Verbindungen herzustellen.
Was passiert, wenn wir uns beim Auftreten des Fehlerereignisses erneut verbinden wollen?
Dank der Verwendung des Redis-Clients können wir den Dienst programmgesteuert verbinden und trennen. So ist es möglich eine Funktion auszuführen, die den Redis-Dienst jedes Mal trennt und neu verbindet, wenn das Fehlerereignis aufgerufen wird wie im folgenden Beispiel gezeigt:
const restartRedisService = async () => {
await redisClient.disconnect();
await redisClient.connect();
};
redisClient.on('error', (error) => {
console.log('redis.reconnect.error: ', error);
setTimeout(restartRedisService, 3000);
});
In diesem Fall haben wir die Funktion restartRedisService
in ein setTimeout
eingebettet, um die Wiederherstellung der Verbindung alle 3 Sekunden durchzuführen. Wir stellen fest, dass in den Protokollen der Fehler, der nach den ersten beiden Verbindungsversuchen angezeigt wird nun unterschiedlich ist. Er enthält zusätzliche Informationen wie Code und Port.

Dank error.code
können wir nun filtern und die Funktion restartRedisService
nur dann ausführen, wenn der Fehlercode unbekannt ist. Sollte der Code "ECONNREFUSED" sein, wird unsere Reconnect-Strategie aktiviert.
const restartRedisService = async () => {
await redisClient.disconnect();
await redisClient.connect();
};
redisClient.on('error', (error) => {
console.log('redis.reconnect.error: ', error);
if (!error.code) {
setTimeout(restartRedisService, 3000);
}
});
Die Protokolle, die bis zu diesem Zeitpunkt entstanden sind, werden im Folgenden dargestellt:

Jetzt, wo unsere Reconnect-Strategie funktioniert, ist der letzte Schritt die Ereignisse connect
und ready
zu abonnieren. So können wir protokollieren, wann der Redis-Dienst wieder online ist.
redisClient.on('connect', () =>
logger.info('redis.connection.connected'),
);
redisClient.on('ready', () => logger.info('redis.connection.ready'));
Fazit
Die Standardstrategie zur Konfiguration des Redis-Dienstes ist nicht ausreichend. Es ist wichtig Fehler zu behandeln, da der Client bei einem unbekannten Fehler getrennt und neu verbunden werden muss, damit die Reconnect-Strategie ordnungsgemäß funktioniert. Damit ist unser Prozess zur Implementierung einer Reconnect-Strategie für Redis in NestJS abgeschlossen.
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { redisStore } from 'cache-manager-redis-store';
import { CacheModule, CacheModuleAsyncOptions } from '@nestjs/cache-manager';
import {
RedisClientOptions,
RedisClientType,
RedisFunctions,
RedisModules,
RedisScripts,
} from 'redis';
import { CommonModule } from '../common.module';
import { PinoLogger } from '`NestJS`-pino';
const ONE_SECOND = 1000;
@Module({
imports: [
CacheModule.registerAsync<RedisClientOptions>({
imports: [ConfigModule, CommonModule],
isGlobal: true,
useFactory: async (config: ConfigService, logger: PinoLogger) => {
const options: RedisClientOptions<
RedisModules,
RedisFunctions,
RedisScripts
> = {
socket: {
host: config.get<string>('REDIS_HOST'),
port: config.get<number>('REDIS_PORT'),
reconnectStrategy: (retries) => {
logger.warn(`redis.reconnect.attempt.${retries}`);
return ONE_SECOND * 3;
},
},
disableOfflineQueue: true,
password: config.get<string>('REDIS_PASSWORD'),
};
const store = await redisStore(options);
const redisClient: RedisClientType = store.getClient();
const restartRedisService = async () => {
await redisClient.disconnect();
await redisClient.connect();
};
redisClient.on('connect', () =>
logger.info('redis.connection.connected'),
);
redisClient.on('ready', () => logger.info('redis.connection.ready'));
redisClient.on('error', (error) => {
logger.error({ err: error }, 'redis.connection.error');
if (!error.code) {
setTimeout(restartRedisService, ONE_SECOND * 3);
}
});
return {
store: store,
} as CacheModuleAsyncOptions;
},
inject: [ConfigService, PinoLogger],
}),
],
})
export class CacheConfigModule {}