JavaScript wird oft als Single-Threaded beschrieben, was bedeutet, dass es jeweils eine Aufgabe ausführt. Aber bedeutet das, dass jeder Codeabschnitt völlig isoliert läuft und keine Möglichkeit hat, andere Aufgaben zu erledigen, während er auf asynchrone Vorgänge wie HTTP-Antworten oder Datenbankanfragen wartet? Die Antwort ist Nein! Tatsächlich ermöglichen die Ereignisschleife und Versprechen von JavaScript die effiziente Verarbeitung asynchroner Aufgaben, während anderer Code weiterhin ausgeführt wird.
Die Wahrheit ist, dass Javascript tatsächlich Single-Threaded ist, jedoch kann ein Missverständnis darüber, wie dies funktioniert, zu häufigen Fallstricken führen. Eine solche Falle ist die Verwaltung asynchroner Vorgänge wie API-Anfragen, insbesondere wenn versucht wird, den Zugriff auf gemeinsam genutzte Ressourcen zu kontrollieren, ohne Race Conditions zu verursachen. Lassen Sie uns ein Beispiel aus der Praxis untersuchen und sehen, wie eine schlechte Implementierung zu schwerwiegenden Fehlern führen kann.
Ich bin auf einen Fehler in einer Anwendung gestoßen, der eine Anmeldung bei einem Backend-Dienst erforderlich machte, um Daten zu aktualisieren. Beim Anmelden erhält die App ein Zugriffstoken mit einem angegebenen Ablaufdatum. Nach Ablauf dieses Ablaufdatums mussten wir uns erneut authentifizieren, bevor wir neue Anfragen an den Update-Endpunkt stellen konnten. Die Herausforderung entstand, weil der Anmeldeendpunkt auf maximal eine Anfrage alle fünf Minuten gedrosselt wurde, während der Aktualisierungsendpunkt innerhalb desselben Fünf-Minuten-Fensters häufiger aufgerufen werden musste. Es war wichtig, dass die Logik ordnungsgemäß funktionierte, dennoch wurde der Anmeldeendpunkt innerhalb des Fünf-Minuten-Intervalls gelegentlich mehrmals ausgelöst, was dazu führte, dass der Aktualisierungsendpunkt nicht funktionierte. Obwohl es Zeiten gab, in denen alles wie erwartet funktionierte, stellte dieser zeitweise auftretende Fehler ein größeres Risiko dar, da er zunächst ein falsches Sicherheitsgefühl vermitteln und den Eindruck erwecken konnte, das System würde ordnungsgemäß funktionieren._
Um dieses Beispiel zu veranschaulichen, verwenden wir eine sehr einfache NestJS-App, die die folgenden Dienste umfasst:
Ich werde hier nicht den Code für alle diese Klassen zeigen; Sie finden es direkt im GitHub-Repository. Stattdessen werde ich mich speziell auf die Logik konzentrieren und darauf, was geändert werden muss, damit sie richtig funktioniert.
Der schlechte Ansatz:
@Injectable() export class BadAuthenticationService extends AbstractAuthenticationService { async loginToBackendService() { this.loginInProgress = true; // this is BAD, we are inside a promise, it's asynchronous. it's not synchronous, javascript can execute it whenever it wants try { const response = await firstValueFrom( this.httpService.post(`https://backend-service.com/login`, { password: 'password', }), ); return response; } finally { this.loginInProgress = false; } } async sendProtectedRequest(route: string, data?: unknown) { if (!this.accessToken) { if (this.loginInProgress) { await new Promise((resolve) => setTimeout(resolve, 1000)); return this.sendProtectedRequest(route, data); } try { await this.awsCloudwatchApiService.logLoginCallAttempt(); const { data: loginData } = await this.loginToBackendService(); this.accessToken = loginData.accessToken; } catch (e: any) { console.error(e?.response?.data); throw e; } } try { const response = await firstValueFrom( this.httpService.post(`https://backend-service.com${route}`, data, { headers: { Authorization: `Bearer ${this.accessToken}`, }, }), ); return response; } catch (e: any) { if (e?.response?.data?.statusCode === 401) { this.accessToken = null; return this.sendProtectedRequest(route, data); } console.error(e?.response?.data); throw e; } } }
Im BadAuthenticationService setzt die Methode loginToBackendService this.loginInProgress auf true, wenn eine Anmeldeanforderung initiiert wird. Da diese Methode jedoch asynchron ist, kann nicht garantiert werden, dass der Anmeldestatus sofort aktualisiert wird. Dies könnte innerhalb des Drosselungslimits zu mehreren gleichzeitigen Aufrufen des Anmeldeendpunkts führen.
Wenn sendProtectedRequest erkennt, dass das Zugriffstoken fehlt, prüft es, ob gerade eine Anmeldung durchgeführt wird. Ist dies der Fall, wartet die Funktion eine Sekunde und versucht es dann erneut. Wenn in dieser Zeit jedoch eine weitere Anfrage eintrifft, kann dies zu weiteren Anmeldeversuchen führen. Dies kann zu mehreren Aufrufen des Anmeldeendpunkts führen, der so gedrosselt ist, dass nur ein Anruf pro Minute möglich ist. Infolgedessen kann der Update-Endpunkt zeitweise ausfallen, was zu unvorhersehbarem Verhalten und einem falschen Sicherheitsgefühl führt, wenn das System zeitweise ordnungsgemäß zu funktionieren scheint.
Zusammenfassend lässt sich sagen, dass das Problem in der unsachgemäßen Handhabung asynchroner Vorgänge liegt, die zu potenziellen Race Conditions führt, die die Logik der Anwendung zerstören können.
@Injectable() export class GoodAuthenticationService extends AbstractAuthenticationService { async loginToBackendService() { try { const response = await firstValueFrom( this.httpService.post(`https://backend-service.com/login`, { password: 'password', }), ); return response; } finally { this.loginInProgress = false; } } async sendProtectedRequest(route: string, data?: unknown) { if (!this.accessToken) { if (this.loginInProgress) { await new Promise((resolve) => setTimeout(resolve, 1000)); return this.sendProtectedRequest(route, data); } // Critical: Set the flag before ANY promise call this.loginInProgress = true; try { await this.awsCloudwatchApiService.logLoginCallAttempt(); const { data: loginData } = await this.loginToBackendService(); this.accessToken = loginData.accessToken; } catch (e: any) { console.error(e?.response?.data); throw e; } } try { const response = await firstValueFrom( this.httpService.post(`https://backend-service.com${route}`, data, { headers: { Authorization: `Bearer ${this.accessToken}`, }, }), ); return response; } catch (e: any) { if (e?.response?.data?.statusCode === 401) { this.accessToken = null; return this.sendProtectedRequest(route, data); } console.error(e?.response?.data); throw e; } } }
Im GoodAuthenticationService ist die loginToBackendService-Methode so strukturiert, dass sie die Anmeldelogik effizient verarbeitet. Die wichtigste Verbesserung ist die Verwaltung des loginInProgress-Flags. Es wird nach der Bestätigung, dass kein Zugriffstoken vorhanden ist, und bevor jegliche asynchronen Vorgänge festgelegt. Dadurch wird sichergestellt, dass nach dem Einleiten eines Anmeldeversuchs keine anderen Anmeldeaufrufe gleichzeitig erfolgen können, wodurch mehrere Anfragen an den gedrosselten Anmeldeendpunkt effektiv verhindert werden.
@Injectable() export class BadAuthenticationService extends AbstractAuthenticationService { async loginToBackendService() { this.loginInProgress = true; // this is BAD, we are inside a promise, it's asynchronous. it's not synchronous, javascript can execute it whenever it wants try { const response = await firstValueFrom( this.httpService.post(`https://backend-service.com/login`, { password: 'password', }), ); return response; } finally { this.loginInProgress = false; } } async sendProtectedRequest(route: string, data?: unknown) { if (!this.accessToken) { if (this.loginInProgress) { await new Promise((resolve) => setTimeout(resolve, 1000)); return this.sendProtectedRequest(route, data); } try { await this.awsCloudwatchApiService.logLoginCallAttempt(); const { data: loginData } = await this.loginToBackendService(); this.accessToken = loginData.accessToken; } catch (e: any) { console.error(e?.response?.data); throw e; } } try { const response = await firstValueFrom( this.httpService.post(`https://backend-service.com${route}`, data, { headers: { Authorization: `Bearer ${this.accessToken}`, }, }), ); return response; } catch (e: any) { if (e?.response?.data?.statusCode === 401) { this.accessToken = null; return this.sendProtectedRequest(route, data); } console.error(e?.response?.data); throw e; } } }
@Injectable() export class GoodAuthenticationService extends AbstractAuthenticationService { async loginToBackendService() { try { const response = await firstValueFrom( this.httpService.post(`https://backend-service.com/login`, { password: 'password', }), ); return response; } finally { this.loginInProgress = false; } } async sendProtectedRequest(route: string, data?: unknown) { if (!this.accessToken) { if (this.loginInProgress) { await new Promise((resolve) => setTimeout(resolve, 1000)); return this.sendProtectedRequest(route, data); } // Critical: Set the flag before ANY promise call this.loginInProgress = true; try { await this.awsCloudwatchApiService.logLoginCallAttempt(); const { data: loginData } = await this.loginToBackendService(); this.accessToken = loginData.accessToken; } catch (e: any) { console.error(e?.response?.data); throw e; } } try { const response = await firstValueFrom( this.httpService.post(`https://backend-service.com${route}`, data, { headers: { Authorization: `Bearer ${this.accessToken}`, }, }), ); return response; } catch (e: any) { if (e?.response?.data?.statusCode === 401) { this.accessToken = null; return this.sendProtectedRequest(route, data); } console.error(e?.response?.data); throw e; } } }
git clone https://github.com/zenstok/nestjs-singlethread-trap.git
cd nestjs-singlethread-trap npm install
Um zwei Anfragen mit der guten Version zu simulieren, rufen Sie an:
npm run start
Obwohl JavaScript Single-Threaded ist, kann es asynchrone Aufgaben wie HTTP-Anfragen mithilfe von Versprechen und der Ereignisschleife effizient verarbeiten. Allerdings kann ein unsachgemäßer Umgang mit diesen Versprechen, insbesondere in Szenarien mit gemeinsam genutzten Ressourcen (wie Token), zu Race Conditions und doppelten Aktionen führen.
Die wichtigste Erkenntnis besteht darin, asynchrone Aktionen wie Anmeldungen zu synchronisieren, um solche Fallen zu vermeiden. Stellen Sie immer sicher, dass Ihr Code über laufende Prozesse informiert ist und Anfragen auf eine Weise verarbeitet, die eine ordnungsgemäße Reihenfolge gewährleistet, auch wenn JavaScript hinter den Kulissen Multitasking betreibt.
Wenn Sie dem Rabbit Byte Club noch nicht beigetreten sind, haben Sie jetzt die Chance, Teil einer lebendigen Community aus Software-Enthusiasten, Tech-Gründern und Nicht-Tech-Gründern zu werden. Gemeinsam tauschen wir Wissen aus, lernen voneinander und bereiten uns auf den Aufbau des nächsten großen Startups vor. Begleiten Sie uns noch heute und seien Sie Teil einer spannenden Reise zu Innovation und Wachstum!
Das obige ist der detaillierte Inhalt vonSo vermeiden Sie die Single-Threaded-Falle in JavaScript. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!