Von Cache Breakdown zu Robustheit: singleflight in Go
Emily Parker
Product Engineer · Leapcell

Vorwort
Beim Aufbau von Hochleistungsdiensten ist Caching eine Schlüsseltechnologie zur Optimierung der Datenbanklast und zur Verbesserung der Reaktionsgeschwindigkeit. Die Verwendung von Caching bringt jedoch auch einige Herausforderungen mit sich, darunter der Cache-Zusammenbruch, der ein großes Problem darstellt. Ein Cache-Zusammenbruch kann zu einem Anstieg des Datenbankdrucks, einer Verschlechterung der Datenbankleistung und in schweren Fällen sogar zum Ausfall der Datenbank führen.
In Go bietet das Paket golang.org/x/sync/singleflight
einen Mechanismus, um sicherzustellen, dass gleichzeitige Anfragen für einen bestimmten Schlüssel nur einmal gleichzeitig ausgeführt werden. Dieser Mechanismus verhindert effektiv Cache-Zusammenbruch-Probleme.
Dieser Artikel befasst sich eingehend mit der Verwendung des singleflight
-Pakets in Go. Ausgehend von den Grundlagen des Cache-Zusammenbruch-Problems wird dann das singleflight
-Paket im Detail vorgestellt und gezeigt, wie man es zur Vermeidung von Cache-Zusammenbrüchen einsetzt.
Cache-Zusammenbruch
Cache-Zusammenbruch bezieht sich auf eine Situation, in der unter hoher Last ein Hot-Key plötzlich abläuft, was dazu führt, dass eine große Anzahl von Anfragen direkt auf die Datenbank zugreift, was die Datenbank überlasten und sogar zum Absturz bringen kann.
Übliche Lösungen umfassen:
- Festlegen, dass Hot-Daten niemals ablaufen: Für einige genau definierte Hot-Daten können Sie festlegen, dass sie niemals ablaufen, um sicherzustellen, dass Anfragen den Cache aufgrund des Cache-Ablaufs nicht umgehen und direkt auf die Datenbank zugreifen.
- Verwendung von Mutex-Locks: Um zu verhindern, dass alle Anfragen gleichzeitig die Datenbank abfragen, wenn der Cache abläuft, kann ein Sperrmechanismus verwendet werden, um sicherzustellen, dass nur eine Anfrage die Datenbank abfragt und den Cache aktualisiert, während andere Anfragen warten, bis der Cache aktualisiert wurde, bevor sie darauf zugreifen.
- Proaktive Aktualisierungen: Überwachen Sie die Cache-Nutzung im Hintergrund, und wenn der Cache kurz vor dem Ablaufen steht, aktualisieren Sie ihn asynchron, um seine Ablaufzeit zu verlängern.
Das singleflight-Paket
Package singleflight bietet einen Mechanismus zur Unterdrückung doppelter Funktionsaufrufe.
Dieser Satz stammt aus der offiziellen Dokumentation.
Mit anderen Worten: Wenn mehrere Goroutinen gleichzeitig versuchen, dieselbe Funktion (basierend auf einem bestimmten Schlüssel) aufzurufen, stellt singleflight sicher, dass die Funktion nur von der zuerst eintreffenden Goroutine ausgeführt wird. Die anderen Goroutinen warten auf das Ergebnis dieses Aufrufs und teilen das Ergebnis, anstatt mehrere Aufrufe gleichzeitig zu initiieren.
Kurz gesagt, singleflight fasst mehrere Anfragen zu einer einzigen Anfrage zusammen, sodass sich mehrere Anfragen dasselbe Ergebnis teilen können.
Komponenten
-
Group: Dies ist die Kernstruktur des singleflight-Pakets. Es verwaltet alle Anfragen und stellt sicher, dass zu jedem Zeitpunkt Anfragen für dieselbe Ressource nur einmal ausgeführt werden. Das Group-Objekt muss nicht explizit erstellt werden; Sie können es einfach deklarieren und verwenden.
-
Do-Methode: Die Group-Struktur bietet die Do-Methode, die die Hauptmethode zum Zusammenführen von Anfragen ist. Diese Methode akzeptiert zwei Argumente: einen String-Schlüssel (zur Identifizierung der Ressource) und eine Funktion
fn
, die die eigentliche Aufgabe ausführt. Wenn Sie Do aufrufen und bereits eine Anfrage mit demselben Schlüssel ausgeführt wird, wartet Do, bis diese Anfrage abgeschlossen ist, und teilt deren Ergebnis; andernfalls führt esfn
aus und gibt das Ergebnis zurück. -
Die Do-Methode hat drei Rückgabewerte. Die ersten beiden sind die Rückgabewerte von
fn
, vom Typinterface{}
bzw.error
. Der letzte Rückgabewert ist ein boolescher Wert, der angibt, ob das Ergebnis von Do von mehreren Aufrufen gemeinsam genutzt wurde. -
DoChan: Diese Methode ähnelt Do, gibt aber einen Kanal zurück, der das Ergebnis empfängt, wenn der Vorgang abgeschlossen ist. Der Rückgabewert ist ein Kanal, was bedeutet, dass wir auf nicht blockierende Weise auf das Ergebnis warten können.
-
Forget: Diese Methode wird verwendet, um einen Schlüssel und seine zugehörigen Anfragedatensätze aus der Group zu löschen, um sicherzustellen, dass der nächste Do-Aufruf mit demselben Schlüssel eine neue Anfrage ausführt, anstatt das vorherige Ergebnis wiederzuverwenden.
-
Result: Dies ist der Strukturtyp, der von der DoChan-Methode zurückgegeben wird. Er kapselt das Ergebnis einer Anfrage und enthält drei Felder:
Val
(interface{}): Das von der Anfrage zurückgegebene Ergebnis.Err
(error): Alle Fehlerinformationen, die während der Anfrage aufgetreten sind.Shared
(bool): Gibt an, ob das Ergebnis mit anderen Anfragen als der aktuellen geteilt wurde.
Installation
Installieren Sie die singleflight-Abhängigkeit in Ihrer Go-Anwendung mit dem folgenden Befehl:
go get golang.org/x/sync/singleflight
Beispielhafte Nutzung
package main import ( "errors" "fmt" "golang.org/x/sync/singleflight" "sync" ) var errRedisKeyNotFound = errors.New("redis: key not found") func fetchDataFromCache() (any, error) { fmt.Println("fetch data from cache") return nil, errRedisKeyNotFound } func fetchDataFromDataBase() (any, error) { fmt.Println("fetch data from database") return "Leapcell", nil } func fetchData() (any, error) { cache, err := fetchDataFromCache() if err != nil && errors.Is(err, errRedisKeyNotFound) { fmt.Println(errRedisKeyNotFound.Error()) return fetchDataFromDataBase() } return cache, err } func main() { var ( sg singleflight.Group wg sync.WaitGroup ) for range 5 { wg.Add(1) go func() { defer wg.Done() v, err, shared := sg.Do("key", fetchData) if err != nil { panic(err) } fmt.Printf("v: %v, shared: %v\n", v, shared) }() } wg.Wait() }
Dieser Code simuliert ein typisches Szenario für gleichzeitigen Zugriff: Abrufen von Daten aus dem Cache und, falls der Cache fehlt, Abrufen aus der Datenbank. Während dieses Prozesses spielt die singleflight-Bibliothek eine entscheidende Rolle. Sie stellt sicher, dass beim gleichzeitigen Zugriff mehrerer gleichzeitiger Anfragen auf dieselben Daten der tatsächliche Abrufvorgang (entweder aus dem Cache oder aus der Datenbank) nur einmal durchgeführt wird. Dies reduziert nicht nur die Datenbanklast, sondern verhindert auch effektiv Cache-Zusammenbrüche in Szenarien mit hoher Parallelität.
Die Ausgabe des Codes ist wie folgt:
fetch data from cache redis: key not found fetch data from database v: Leapcell, shared: true v: Leapcell, shared: true v: Leapcell, shared: true v: Leapcell, shared: true v: Leapcell, shared: true
Wie gezeigt, wird der Datenabrufvorgang, wenn 5 Goroutinen gleichzeitig dieselben Daten abrufen, tatsächlich nur einmal von einer Goroutine durchgeführt. Da außerdem alle zurückgegebenen Shared-Werte true sind, bedeutet dies, dass das Ergebnis mit den anderen 4 Goroutinen geteilt wurde.
Bewährte Verfahren
Schlüsseldesign
Beim Generieren von Schlüsseln sollten wir deren Eindeutigkeit und Konsistenz gewährleisten.
- Eindeutigkeit: Stellen Sie sicher, dass der an die Do-Methode übergebene Schlüssel eindeutig ist, damit die Group zwischen verschiedenen Anfragen unterscheiden kann. Es wird empfohlen, eine strukturierte Namenskonvention für Schlüssel zu verwenden, z. B.
{type}:{identifier}
. Wenn Sie beispielsweise Benutzerinformationen abrufen, kann der Schlüsseluser:1234
lauten, wobeiuser
den Datentyp und1234
die spezifische Benutzerkennung angibt. - Konsistenz: Für dieselbe Anfrage sollte der generierte Schlüssel immer konsistent sein, unabhängig davon, wann er aufgerufen wird. Dadurch kann die Group identische Anfragen ordnungsgemäß zusammenführen und unerwartete Fehler verhindern.
Timeout-Kontrolle
Beim Aufruf von Group.Do kann die zuerst eintreffende Goroutine die Funktion fn
erfolgreich ausführen, während alle nachfolgenden Goroutinen blockiert werden. Wenn der blockierte Zustand zu lange anhält, ist möglicherweise eine Downgrade-Strategie erforderlich, um sicherzustellen, dass das System reaktionsfähig bleibt. In solchen Fällen können wir Group.DoChan in Kombination mit der select
-Anweisung verwenden, um die Timeout-Kontrolle zu implementieren.
Im Folgenden finden Sie ein einfaches Beispiel, das die Timeout-Kontrolle demonstriert:
package main import ( "fmt" "golang.org/x/sync/singleflight" "time" ) func main() { var sg singleflight.Group doChan := sg.DoChan("key", func() (interface{}, error) { time.Sleep(4 * time.Second) return "Leapcell", nil }) select { case <-doChan: fmt.Println("done") case <-time.After(2 * time.Second): fmt.Println("timeout") // Implementieren Sie hier andere Downgrade-Strategien } }
Zusammenfassung
- In diesem Artikel wurde zunächst das Konzept des Cache-Zusammenbruchs und seine gängigen Lösungen vorgestellt.
- Anschließend wurde das singleflight-Paket eingehend untersucht, einschließlich seiner grundlegenden Konzepte, Komponenten, Installation und Anwendungsbeispiele.
- Als Nächstes wurde anhand eines simulierten Beispiels für gleichzeitigen Zugriff gezeigt, wie singleflight verwendet werden kann, um Cache-Zusammenbrüche in Szenarien mit hoher Parallelität zu verhindern.
- Abschließend wurden die besten Verfahren für das Entwerfen von Schlüsseln und das Steuern von Anfrage-Timeouts in der Praxis erörtert, um das Verständnis und die Anwendung von singleflight zur Optimierung der Logik für die gleichzeitige Verarbeitung zu verbessern.
Wir sind Leapcell, Ihre erste Wahl für das Hosten von Go-Projekten.
Leapcell ist die Serverless-Plattform der nächsten Generation für Webhosting, asynchrone Aufgaben und Redis:
Multi-Language-Unterstützung
- Entwickeln Sie mit Node.js, Python, Go oder Rust.
Stellen Sie unbegrenzt viele Projekte kostenlos bereit
- Zahlen Sie nur für die Nutzung - keine Anfragen, keine Gebühren.
Unschlagbare Kosteneffizienz
- Pay-as-you-go ohne Leerlaufgebühren.
- Beispiel: 25 US-Dollar unterstützen 6,94 Millionen Anfragen bei einer durchschnittlichen Antwortzeit von 60 ms.
Optimierte Entwicklererfahrung
- Intuitive Benutzeroberfläche für mühelose Einrichtung.
- Vollautomatische CI/CD-Pipelines und GitOps-Integration.
- Echtzeitmetriken und -protokollierung für umsetzbare Erkenntnisse.
Mühelose Skalierbarkeit und hohe Leistung
- Auto-Scaling zur mühelosen Bewältigung hoher Parallelität.
- Null Betriebsaufwand - konzentrieren Sie sich einfach auf den Aufbau.
Erfahren Sie mehr in der Dokumentation!
Folgen Sie uns auf X: @LeapcellHQ