Einführung
In diesem Projekt werden wir die Grundprinzipien und Bedeutung des Cachens kennenlernen und anschließend einen Caching-Component mit der Go-Programmiersprache entwerfen und implementieren.
Caching ist eine weit verbreitete Technik in Computersystemen, um die Leistung zu verbessern, indem häufig abgerufenes Daten im Arbeitsspeicher gespeichert werden. Dies ermöglicht eine schnellere Abrufung und reduziert den Bedarf, auf langsamere Datenquellen wie Datenbanken oder Remote-Services zuzugreifen.
In diesem Projekt werden wir die Grundprinzipien und Vorteile des Cachens kennenlernen. Wir werden auch einen Caching-Component mit der Go-Programmiersprache entwerfen und implementieren. Der Caching-Component wird Funktionen wie das Speichern von gecachten Daten, das Verwalten von abgelaufenen Datenobjekten, das Importieren und Exportieren von Daten sowie CRUD-Operationen (Create, Read, Update, Delete) haben.
Durch die Fertigstellung dieses Projekts werden Sie Kenntnisse und Fähigkeiten im Bereich der Caching-Grundprinzipien, Datenstrukturen und Go-Programmierung erwerben. Dies wird es Ihnen ermöglichen, effiziente und leistungsstarke Softwaresysteme zu entwickeln, die die Caching-Techniken effektiv nutzen.
🎯 Aufgaben
In diesem Projekt werden Sie lernen:
- Wie man die Grundprinzipien und Bedeutung des Cachens versteht
- Wie man ein Caching-System entwirft, um Daten im Arbeitsspeicher zu speichern und zu verwalten
- Wie man CRUD-Operationen und die Ablaufverwaltung für das Caching-System implementiert
- Wie man Funktionen zum Importieren und Exportieren von Daten aus dem Caching-System hinzufügt
🏆 Errungenschaften
Nach Abschluss dieses Projekts werden Sie in der Lage sein:
- Die Grundprinzipien und Vorteile des Cachens zu erklären
- Ein Caching-System auf der Grundlage solider Entwurfsprinzipien zu entwerfen
- Effiziente Datenstrukturen und Algorithmen für die Cacheverwaltung zu implementieren
- CRUD-Operationen in Go für das Caching-System zu entwickeln
- Daten für Import- und Exportvorgänge zu serialisieren und zu deserialisieren
Was ist ein Cache?
Caches sind in der Computerhardware weit verbreitet. Beispielsweise haben CPUs einen Cache der ersten Stufe, einen Cache der zweiten Stufe und sogar einen Cache der dritten Stufe. Das Prinzip des Cachebetriebs ist, dass wenn die CPU Daten lesen muss, sie zuerst in den Cache nach den benötigten Daten sucht. Wenn diese gefunden werden, werden sie direkt verarbeitet. Wenn nicht, werden die Daten aus dem Arbeitsspeicher gelesen. Aufgrund der höheren Geschwindigkeit des Caches in der CPU im Vergleich zum Arbeitsspeicher kann die Verwendung des Caches die CPU-Verarbeitungsgeschwindigkeit beschleunigen. Caching existiert nicht nur in der Hardware, sondern auch in verschiedenen Softwaresystemen. Beispielsweise existieren in Web-Systemen Caches auf Servern, Clients oder Proxy-Servern. Das weit verbreitete CDN (Content Delivery Network) kann auch als ein riesiger Caching-System angesehen werden. Es gibt viele Vorteile bei der Verwendung von Caching in Web-Systemen, wie die Reduzierung des Netzwerkverkehrs, die Verringerung der Zuglatenzzeit der Clients und die Verringerung der Serverlast.
Derzeit gibt es viele leistungsstarke Caching-Systeme, wie Memcache, Redis usw. Insbesondere Redis wird heutzutage in verschiedenen Web-Diensten weit verbreitet eingesetzt. Da es bereits diese featurereichen Caching-Systeme gibt, warum müssen wir noch unser eigenes Caching-System implementieren? Es gibt zwei Hauptgründe dafür. Erstens, indem wir es selbst implementieren, können wir das Arbeitsprinzip des Caching-Systems verstehen, was ein klassischer Grund ist. Zweitens existieren Caching-Systeme wie Redis unabhängig voneinander. Wenn wir nur eine einfache Anwendung entwickeln müssen, kann die Verwendung eines separaten Redis-Servers zu komplex sein. In diesem Fall wäre es am besten, wenn es ein featurereiches Softwarepaket gibt, das diese Funktionen implementiert. Indem wir einfach dieses Softwarepaket importieren, können wir die Caching-Funktionalität erreichen, ohne dass ein separater Redis-Server erforderlich ist.
Design des Cache-Systems
In einem Cache-System werden die gecachten Daten normalerweise im Arbeitsspeicher gespeichert. Daher sollte das Cache-System, das wir entwerfen, die Daten im Arbeitsspeicher auf bestimmte Weise verwalten. Wenn das System heruntergefahren wird, gehen die Daten nicht verloren? Tatsächlich unterstützt das Cache-System in den meisten Fällen auch das Schreiben der Daten im Arbeitsspeicher in eine Datei. Wenn das System neu gestartet wird, können die Daten in der Datei wieder in den Arbeitsspeicher geladen werden. Auf diese Weise gehen die Cachedaten auch bei einem Systemausfall nicht verloren.
Zugleich bietet das Cache-System auch einen Mechanismus zum Bereinigen von abgelaufenen Daten. Dies bedeutet, dass jedes Datenobjekt im Cache eine Lebensdauer hat. Wenn ein Datenobjekt abläuft, wird es aus dem Arbeitsspeicher gelöscht. Dadurch sind immer die aktuellen Daten verfügbar, während die inaktiven Daten gelöscht werden, da es nicht notwendig ist, sie zu cachen.
Das Cache-System muss auch Schnittstellen für externe Operationen bereitstellen, sodass andere Komponenten des Systems auf den Cache zugreifen können. Typischerweise muss das Cache-System CRUD-Operationen unterstützen, die das Erstellen (Hinzufügen), Lesen, Aktualisieren und Löschen umfassen.
Aufgrund der obigen Analyse können wir zusammenfassen, dass das Cache-System die folgenden Funktionen haben muss:
- Speicherung von gecachten Daten
- Verwaltung von abgelaufenen Datenobjekten
- Importieren und Exportieren von Daten aus dem Arbeitsspeicher
- Bereitstellung von CRUD-Schnittstellen.
Entwicklungsvorbereitung
Zunächst erstellen Sie ein Arbeitsverzeichnis und legen Sie die Umgebungsvariable GOPATH fest:
cd ~/project/
mkdir -p golang/src
export GOPATH=~/project/golang
In den obigen Schritten erstellen wir das Verzeichnis ~/project/golang und legen es als GOPATH für die nachfolgenden Experimente fest.
Grundstruktur eines Cache-Systems
Gecachte Daten müssen im Arbeitsspeicher gespeichert werden, um schnell abgerufen werden zu können. Welche Datenstruktur sollte verwendet werden, um die Datenobjekte zu speichern? Im Allgemeinen wird eine Hashtabelle verwendet, um die Datenobjekte zu speichern, da dies eine bessere Leistung bei der Datenabrufung bietet. In der Go-Programmiersprache müssen wir nicht unsere eigene Hashtabelle implementieren, da der eingebautes Typ map bereits eine Hashtabelle implementiert. Wir können daher die Cachedatenobjekte direkt in einer map speichern.
Da das Cache-System auch das Bereinigen von abgelaufenen Daten unterstützt, sollten die Cachedatenobjekte eine Lebensdauer haben. Dies bedeutet, dass die Cachedatenobjekte kapselt und im Cache-System gespeichert werden müssen. Um dies zu tun, müssen wir zunächst das Cachedatenobjekt implementieren. Erstellen Sie ein neues Verzeichnis cache im Verzeichnis GOPATH/src, und erstellen Sie eine Quelltextdatei cache.go:
package cache
import (
"encoding/gob"
"fmt"
"io"
"os"
"sync"
"time"
)
type Item struct {
Object interface{} // Das tatsächliche Datenobjekt
Expiration int64 // Lebensdauer
}
// Überprüfen, ob das Datenobjekt abgelaufen ist
func (item Item) Expired() bool {
if item.Expiration == 0 {
return false
}
return time.Now().UnixNano() > item.Expiration
}
In obigem Code definieren wir eine Item-Struktur, die zwei Felder hat. Object wird verwendet, um Datenobjekte beliebiger Typen zu speichern, und Expiration speichert die Ablaufzeit des Datenobjekts. Wir bieten auch eine Expired()-Methode für den Item-Typ an, die einen booleschen Wert zurückgibt, der angibt, ob das Datenobjekt abgelaufen ist. Es ist wichtig zu beachten, dass die Ablaufzeit eines Datenobjekts ein Unix-Zeitstempel in Nanosekunden ist. Wie bestimmen wir, ob ein Datenobjekt abgelaufen ist? Es ist tatsächlich ziemlich einfach. Wir protokollieren die Ablaufzeit jedes Datenobjekts, und das Cache-System überprüft periodisch jedes Datenobjekt. Wenn die Ablaufzeit eines Datenobjekts früher als die aktuelle Zeit ist, wird das Datenobjekt aus dem Cache-System entfernt. Um dies zu tun, verwenden wir das time-Modul, um periodische Aufgaben zu implementieren.
Mit diesem können wir jetzt den Rahmen des Cache-Systems implementieren. Der Code lautet wie folgt:
const (
// Flag für keine Ablaufzeit
NoExpiration time.Duration = -1
// Standard-Ablaufzeit
DefaultExpiration time.Duration = 0
)
type Cache struct {
defaultExpiration time.Duration
items map[string]Item // Speichert Cachedatenobjekte in einer map
mu sync.RWMutex // Leseschreib-Sperre
gcInterval time.Duration // Intervall für das Bereinigen von abgelaufenen Daten
stopGc chan bool
}
// Bereinigt abgelaufene Cachedatenobjekte
func (c *Cache) gcLoop() {
ticker := time.NewTicker(c.gcInterval)
for {
select {
case <-ticker.C:
c.DeleteExpired()
case <-c.stopGc:
ticker.Stop()
return
}
}
}
In obigem Code haben wir die Cache-Struktur implementiert, die die Cache-Systemstruktur darstellt. Das items-Feld ist eine map, die verwendet wird, um die Cachedatenobjekte zu speichern. Wie Sie sehen können, haben wir auch die gcLoop()-Methode implementiert, die die DeleteExpired()-Methode planmäßig mit einem time.Ticker ausführt. Ein mit time.NewTicker() erstellter ticker sendet Daten von seinem ticker.C-Kanal in einem bestimmten gcInterval-Intervall. Wir können diese Eigenschaft nutzen, um die DeleteExpired()-Methode periodisch auszuführen.
Um sicherzustellen, dass die gcLoop()-Funktion normal beendet werden kann, hören wir auf Daten aus dem c.stopGc-Kanal. Wenn Daten an diesen Kanal gesendet werden, stoppen wir die Ausführung von gcLoop(). Beachten Sie auch, dass wir die NoExpiration- und DefaultExpiration- Konstanten definieren, wobei die erstere einen Datenobjekttyp darstellt, der nie abläuft, und die letztere einen Datenobjekttyp mit einer Standard-Ablaufzeit darstellt. Wie implementieren wir DeleteExpired()? Siehe den folgenden Code:
// Löscht ein Cachedatenobjekt
func (c *Cache) delete(k string) {
delete(c.items, k)
}
// Löscht abgelaufene Datenobjekte
func (c *Cache) DeleteExpired() {
now := time.Now().UnixNano()
c.mu.Lock()
defer c.mu.Unlock()
for k, v := range c.items {
if v.Expiration > 0 && now > v.Expiration {
c.delete(k)
}
}
}
Wie Sie sehen können, ist die DeleteExpired()-Methode ziemlich einfach. Wir müssen nur alle Datenobjekte durchlaufen und die abgelaufenen löschen.
Implementierung der CRUD-Schnittstelle für das Cache-System
Jetzt können wir die CRUD-Schnittstelle für das Cache-System implementieren. Wir können Daten in das Cache-System hinzufügen, indem wir die folgenden Schnittstellen verwenden:
// Setzt ein Cachedatenobjekt, überschreibt es, wenn es existiert
func (c *Cache) Set(k string, v interface{}, d time.Duration) {
var e int64
if d == DefaultExpiration {
d = c.defaultExpiration
}
if d > 0 {
e = time.Now().Add(d).UnixNano()
}
c.mu.Lock()
defer c.mu.Unlock()
c.items[k] = Item{
Object: v,
Expiration: e,
}
}
// Setzt ein Datenobjekt ohne Sperroperation
func (c *Cache) set(k string, v interface{}, d time.Duration) {
var e int64
if d == DefaultExpiration {
d = c.defaultExpiration
}
if d > 0 {
e = time.Now().Add(d).UnixNano()
}
c.items[k] = Item{
Object: v,
Expiration: e,
}
}
// Ruft ein Datenobjekt ab, muss auch überprüfen, ob das Objekt abgelaufen ist
func (c *Cache) get(k string) (interface{}, bool) {
item, found := c.items[k]
if!found {
return nil, false
}
if item.Expired() {
return nil, false
}
return item.Object, true
}
// Fügt ein Datenobjekt hinzu, gibt einen Fehler zurück, wenn das Objekt bereits existiert
func (c *Cache) Add(k string, v interface{}, d time.Duration) error {
c.mu.Lock()
_, found := c.get(k)
if found {
c.mu.Unlock()
return fmt.Errorf("Item %s already exists", k)
}
c.set(k, v, d)
c.mu.Unlock()
return nil
}
// Ruft ein Datenobjekt ab
func (c *Cache) Get(k string) (interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
item, found := c.items[k]
if!found {
return nil, false
}
if item.Expired() {
return nil, false
}
return item.Object, true
}
In obigem Code haben wir die Set()- und Add()-Schnittstellen implementiert. Der Hauptunterschied zwischen den beiden ist, dass die erstere das Datenobjekt im Cache-System überschreibt, wenn es bereits existiert, während die letztere einen Fehler ausgibt, wenn das Datenobjekt bereits existiert, um das Versehentliche Überschreiben des Caches zu vermeiden. Wir haben auch die Get()-Methode implementiert, die das Datenobjekt aus dem Cache-System abruft. Es ist wichtig zu beachten, dass die wahre Bedeutung des Vorhandenseins eines Cachedatenobjekts darin besteht, dass das Objekt existiert und nicht abgelaufen ist.
Als Nächstes können wir die Lösch- und Aktualisierungsschnittstellen implementieren.
// Ersetzt ein vorhandenes Datenobjekt
func (c *Cache) Replace(k string, v interface{}, d time.Duration) error {
c.mu.Lock()
_, found := c.get(k)
if!found {
c.mu.Unlock()
return fmt.Errorf("Item %s doesn't exist", k)
}
c.set(k, v, d)
c.mu.Unlock()
return nil
}
// Löscht ein Datenobjekt
func (c *Cache) Delete(k string) {
c.mu.Lock()
c.delete(k)
c.mu.Unlock()
}
Der obige Code spricht für sich selbst, daher gehe ich nicht zu viel in die Einzelheiten.
Import und Export des Cache-Systems
Wir haben zuvor erwähnt, dass das Cache-System das Importieren von Daten in eine Datei und das Laden von Daten aus einer Datei unterstützt. Lassen Sie uns jetzt diese Funktionalität implementieren.
// Schreibt Cachedatenobjekte in einen io.Writer
func (c *Cache) Save(w io.Writer) (err error) {
enc := gob.NewEncoder(w)
defer func() {
if x := recover(); x!= nil {
err = fmt.Errorf("Error registering item types with Gob library")
}
}()
c.mu.RLock()
defer c.mu.RUnlock()
for _, v := range c.items {
gob.Register(v.Object)
}
err = enc.Encode(&c.items)
return
}
// Speichert Datenobjekte in einer Datei
func (c *Cache) SaveToFile(file string) error {
f, err := os.Create(file)
if err!= nil {
return err
}
if err = c.Save(f); err!= nil {
f.Close()
return err
}
return f.Close()
}
// Liest Datenobjekte aus einem io.Reader
func (c *Cache) Load(r io.Reader) error {
dec := gob.NewDecoder(r)
items := map[string]Item{}
err := dec.Decode(&items)
if err == nil {
c.mu.Lock()
defer c.mu.Unlock()
for k, v := range items {
ov, found := c.items[k]
if!found || ov.Expired() {
c.items[k] = v
}
}
}
return err
}
// Lädt Cachedatenobjekte aus einer Datei
func (c *Cache) LoadFile(file string) error {
f, err := os.Open(file)
if err!= nil {
return err
}
if err = c.Load(f); err!= nil {
f.Close()
return err
}
return f.Close()
}
In obigem Code codiert die Save()-Methode die binären Cachedaten mit dem gob-Modul und schreibt sie in ein Objekt, das das io.Writer-Interface implementiert. Andererseits liest die Load()-Methode binäre Daten aus einem io.Reader und deserialisiert dann die Daten mit dem gob-Modul. Im Wesentlichen serialisieren und deserialisieren wir hier die Cachedaten.
Weitere Schnittstellen des Cache-Systems
Bisher ist die Funktionalität des gesamten Cache-Systems abgeschlossen, und der Großteil der Arbeit ist erledigt. Lassen Sie uns die letzten Aufgaben abschließen.
// Gibt die Anzahl der gecachten Datenobjekte zurück
func (c *Cache) Count() int {
c.mu.RLock()
defer c.mu.RUnlock()
return len(c.items)
}
// Leert den Cache
func (c *Cache) Flush() {
c.mu.Lock()
defer c.mu.Unlock()
c.items = map[string]Item{}
}
// Stoppt das Bereinigen von abgelaufenen Caches
func (c *Cache) StopGc() {
c.stopGc <- true
}
// Erstellt ein neues Cache-System
func NewCache(defaultExpiration, gcInterval time.Duration) *Cache {
c := &Cache{
defaultExpiration: defaultExpiration,
gcInterval: gcInterval,
items: map[string]Item{},
stopGc: make(chan bool),
}
// Startet die Goroutine zum Bereinigen von abgelaufenen Daten
go c.gcLoop()
return c
}
In obigem Code haben wir mehrere Methoden hinzugefügt. Count() gibt die Anzahl der im System gecachten Datenobjekte zurück, Flush() leert das gesamte Cache-System, und StopGc() stoppt das Cache-System bei der Bereinigung von abgelaufenen Datenobjekten. Schließlich können wir mit der NewCache()-Methode ein neues Cache-System erstellen.
Bisher ist das gesamte Cache-System abgeschlossen. Es ist ziemlich einfach, oder? Lassen Sie uns jetzt einige Tests durchführen.
Testen des Cache-Systems
Wir werden ein Beispielprogramm schreiben, dessen Quellcode sich im Verzeichnis ~/project/golang/src/cache/sample/sample.go befindet. Der Inhalt des Programms lautet wie folgt:
package main
import (
"cache"
"fmt"
"time"
)
func main() {
defaultExpiration, _ := time.ParseDuration("0.5h")
gcInterval, _ := time.ParseDuration("3s")
c := cache.NewCache(defaultExpiration, gcInterval)
k1 := "hello labex"
expiration, _ := time.ParseDuration("5s")
c.Set("k1", k1, expiration)
s, _ := time.ParseDuration("10s")
if v, found := c.Get("k1"); found {
fmt.Println("Found k1: ", v)
} else {
fmt.Println("Not found k1")
}
// Pause for 10 seconds
time.Sleep(s)
// Now k1 should have been cleared
if v, found := c.Get("k1"); found {
fmt.Println("Found k1: ", v)
} else {
fmt.Println("Not found k1")
}
}
Der Beispielcode ist sehr einfach. Wir erstellen ein Cache-System mit der NewCache-Methode, mit einem Zeitintervall von 3 Sekunden für das Bereinigen von abgelaufenen Daten und einer Standard-Ablaufzeit von einer halben Stunde. Anschließend legen wir ein Datenobjekt "k1" mit einer Ablaufzeit von 5 Sekunden fest. Nachdem das Datenobjekt gesetzt wurde, holen wir es sofort ab und pausieren dann für 10 Sekunden. Wenn wir "k1" erneut abrufen, sollte es bereits gelöscht sein. Das Programm kann mit dem folgenden Befehl ausgeführt werden:
cd ~/project/golang/src/cache
go mod init
go run sample/sample.go
Die Ausgabe wird wie folgt aussehen:
Found k1: hello labex
Not found k1
Zusammenfassung
In diesem Projekt haben wir ein Cache-System entwickelt, das das Hinzufügen, Löschen, Ersetzen und Abfragen von Datenobjekten ermöglicht. Es umfasst auch die Möglichkeit, abgelaufene Daten zu löschen. Wenn Sie mit Redis vertraut sind, wissen Sie möglicherweise, dass es die Möglichkeit bietet, einen numerischen Wert, der einem Schlüssel zugeordnet ist, zu erhöhen. Beispielsweise kann der Wert von "key", der auf "20" gesetzt ist, auf "22" erhöht werden, indem dem Increment-Interface der Parameter "2" übergeben wird. Möchten Sie versuchen, diese Funktionalität mit unserem Cache-System zu implementieren?



