Dies ist Teil drei von fünf einer Tutorialserie zum Testen von datenintensivem Code mit Go. In Teil zwei habe ich Tests mit einer echten In-Memory-Datenschicht basierend auf der beliebten SQLite durchgeführt. In diesem Lernprogramm werde ich einen Vergleich mit einer lokalen komplexen Datenschicht durchführen, die eine relationale Datenbank und einen Redis-Cache enthält.
Das Testen gegen eine In-Memory-Datenschicht ist großartig. Die Tests sind blitzschnell und Sie haben die volle Kontrolle. Manchmal müssen Sie jedoch näher an der tatsächlichen Konfiguration Ihrer Produktionsdatenschicht sein. Hier sind einige mögliche Gründe:
Ich bin sicher, es gibt andere Gründe, aber Sie können sehen, warum die Verwendung einer In-Memory-Datenschicht zum Testen in vielen Fällen nicht ausreichend ist.
OK. Wir möchten also eine tatsächliche Datenschicht testen. Wir wollen aber trotzdem so leicht und agil wie möglich sein. Dies bedeutet eine lokale Datenschicht. Hier sind die Vorteile:
In diesem Tutorial werden wir den Vorzug geben. Wir implementieren (sehr teilweise) eine hybride Datenschicht, die aus einer relationalen MariaDB-Datenbank und einem Redis-Server besteht. Dann verwenden wir Docker, um eine lokale Datenschicht aufzubauen, die wir in unseren Tests verwenden können.
Zuerst brauchen Sie natürlich Docker. Lesen Sie die Dokumentation, wenn Sie mit Docker nicht vertraut sind. Im nächsten Schritt erhalten Sie Bilder für unsere Datenspeicher: MariaDB und Redis. Ohne zu sehr ins Detail zu gehen, ist MariaDB eine großartige relationale Datenbank, die mit MySQL kompatibel ist, und Redis ist ein großartiger Speicher für die Speicherung von In-Memory-Schlüsseln (und vieles mehr)..
> docker pull mariadb…> docker pull redis…> docker images REPOSITORY TAG BILD-ID ERSTELLT GRÖSSE mariadb latest 51d6a5e69fa7 vor 2 Wochen 402MB redis latest b6dddb991dfa vor 2 Wochen 107MB
Nun, da wir Docker installiert haben und die Images für MariaDB und Redis haben, können wir eine docker-compose.yml-Datei schreiben, mit der wir unsere Datenspeicher starten. Nennen wir unsere DB "songify".
mariadb-songify: image: mariadb: Letzter Befehl:> --general-log --general-log-file = / var / log / mysql / query.log expose: - "3306" Ports: - "3306: 3306" - Umgebung : MYSQL_DATABASE: "songify" MYSQL_ALLOW_EMPTY_PASSWORD: "true" volume_from: - mariadb-data mariadb-data: image: mariadb: neueste volumes: - / var / lib / mysql enterypoint: / bin / bash redis: image: redis expose: - " 6379 "Ports: -" 6379: 6379 "
Sie können Ihre Datenspeicher mit der starten Docker zusammenstellen
Befehl (ähnlich wie vagrant up
). Die Ausgabe sollte so aussehen:
> docker-compose up Hybridtest_redis_1 wird gestartet ... Hybridtest_mariadb-data_1 wird gestartet ... Hybridtest_redis_1 wird gestartet Hybridtest_mariadb-data_1 wird gestartet ... abgeschlossen Hybridtest_mariadb-songify_1 wird gestartet ... * DB von der Festplatte geladen: 0,002 Sekunden redis_1 | * Bereit, Verbindungen anzunehmen… mariadb-songify_1 | [Anmerkung] mysqld: bereit für Verbindungen…
Zu diesem Zeitpunkt verfügen Sie über einen voll ausgestatteten MariaDB-Server, der Port 3306 überwacht, und einen Redis-Server, der Port 6379 überwacht (beide sind die Standardports)..
Lassen Sie uns diese leistungsstarken Datenspeicher nutzen und unsere Datenschicht zu einer hybriden Datenschicht aufrüsten, die Songs in Redis pro Benutzer zwischenspeichert. Wann GetSongsByUser ()
aufgerufen wird, überprüft die Datenschicht zunächst, ob Redis die Songs bereits für den Benutzer speichert. Wenn dies der Fall ist, geben Sie einfach die Songs von Redis zurück. Wenn dies nicht der Fall ist (Cache-Miss), werden die Songs von MariaDB abgerufen und der Redis-Cache gefüllt, sodass er für das nächste Mal bereit ist.
Hier ist die Struktur- und Konstruktordefinition. Die Struktur behält einen DB-Handle wie zuvor und auch einen Redis-Client. Der Konstruktor stellt eine Verbindung zur relationalen Datenbank sowie zu Redis her. Es erstellt das Schema und wird nur dann erneut gelöscht, wenn die entsprechenden Parameter erfüllt sind. Dies ist nur für das Testen erforderlich. In der Produktion erstellen Sie das Schema einmal (ignorieren Schema-Migrationen)..
type HybridDataLayer struct db * sql.DB redis * redis.Client func NewHybridDataLayer (dbHost-Zeichenfolge, dbPort int, redisHost-Zeichenfolge, createSchema-Bool, clearRedis-Bool) (* HybridDataLayer, Fehler) dsn: = fmt.Sprintf ("root @ tcp (% s:% d) / ", dbHost, dbPort) if createSchema err: = createMariaDBSchema (dsn) if err! = null return nil, err db, err: = sql.Open (" mysql ", dsn + "desongcious? parseTime = true") if err! = nil return nil, err redisClient: = redis.NewClient (& redis.Options Addr: redisHost + ": 6379", Passwort: "", DB: 0, ) _, err = redisClient.Ping (). Ergebnis () if err! = nil return nil, err wenn clearRedis redisClient.FlushDB () return & HybridDataLayer db, redisClient, nil
MariaDB und SQLite unterscheiden sich etwas in Bezug auf DDL. Die Unterschiede sind klein, aber wichtig. Go hat kein ausgereiftes Cross-DB-Toolkit wie Pythons fantastische SQLAlchemy. Sie müssen es also selbst verwalten (nein, Gorm zählt nicht). Die Hauptunterschiede sind:
AUTO_INCREMENT
.VARCHAR
anstatt TEXT
.Hier ist der Code:
func createMariaDBSchema (dsn string) error db, err: = sql.Open ("mysql", dsn) if err! = nil return err // Wiederherstellen von DB-Befehlen: = [] string "DROP DATABASE songify;", "CREATE DATABASE songify;", für _, s: = Bereich (Befehle) _, err = db.Exec (s) if err! = Nil return err // Erzeuge Schema db, err = sql.Open ("mysql", dsn + "songify? parseTime = true") if err! = nil return err schema: = [] string 'CREATE TABLE WENN NICHT EXISTEN (ID INTEGER PRIMARY KEY) AUTO_INCREMENT, url VARCHAR (2088) UNIQUE , Titel VARCHAR (100), Beschreibung VARCHAR (500)); ',' CREATE TABLE WENN NOT EXISTS-Benutzer (ID INTEGER PRIMARY KEY) AUTO_INCREMENT, Name VARCHAR (100), E-Mail VARCHAR (100) UNIQUE, registered_at TIMESTAMP, last_login TIMESTAMP); ', "CREATE INDEX user_email_idx ON Benutzer (E-Mail);",' CREATE TABLE WENN NICHT EXISTS-Label (ID INTEGER PRIMARY KEY AUTO_INCREMENT, Name VARCHAR (100) UNIQUE); ', "CREATE INDEX Label_Name_idx ON Label (Name);", 'CREATE TABLE WENN NICHT EXISTS ist. Label_song (label_id INTEGER NOT NULL REFE.) RENCES-Label (ID), song_id INTEGER NOT NULL REFERENCES-Song (ID), PRIMARY KEY (label_id, song_id)); ',' CREATE TABLE WENN NICHT EXISTS ist user_song (user_id INTEGER NICHT NULL REFERENCES-Benutzer (id), song_id INTEGER NOT NULL REFERENCES song (id), PRIMARY KEY (user_id, song_id)); ', für _, s: = range (schema) _, err = db.Exec (s) wenn err! = nil return err return nil
Redis ist sehr einfach von Go zu verwenden. Die Client-Bibliothek "github.com/go-redis/redis" ist sehr intuitiv und folgt den Redis-Befehlen treu. Um beispielsweise zu testen, ob ein Schlüssel existiert, verwenden Sie einfach die Ausgänge ()
Methode des Redis-Clients, die einen oder mehrere Schlüssel akzeptiert und zurückgibt, wie viele davon vorhanden sind.
In diesem Fall überprüfe ich nur einen Schlüssel:
count, err: = m.redis.Exists (email) .Result () wenn err! = null return err
Die Tests sind eigentlich identisch. Die Schnittstelle hat sich nicht geändert, und das Verhalten hat sich nicht geändert. Die einzige Änderung besteht darin, dass die Implementierung jetzt einen Cache in Redis enthält. Das GetSongsByEmail ()
Methode ruft jetzt einfach auf refreshUser_Redis ()
.
func (m * HybridDataLayer) GetSongsByUser (u User) (Titel [] Song, Fehler) err = m.refreshUser_Redis (u.Email, & songs) return
Das refreshUser_Redis ()
Diese Methode gibt die User-Songs von Redis zurück, falls vorhanden, und holt sie anderweitig aus MariaDB.
type Songs * [] Song func (m * HybridDataLayer) refreshUser_Redis (E-Mail-String, Out-Songs) error count, err: = m.redis.Exists (email) .Result () wenn err! = nil return err wenn Anzahl zurückgegeben wird == 0 err = m.getSongsByUser_DB (E-Mail, out) wenn err! = Nil return err für _, song: = range * out s, err: = serializeSong (song) wenn err! = Nil return err _, err = m.redis.SAdd (email, s) .Result () wenn err! = nil return err return Mitglieder, err: = m.redis.SMembers (email) .Result () für _ , member: = Bereichsmitglieder song, err: = deserializeSong ([] byte (member)) wenn err! = nil return err * out = anhängen (* out, song) return out, nil
Aus der Sicht der Testmethodik besteht hier ein kleines Problem. Wenn wir die abstrakte Datenschicht-Schnittstelle testen, haben wir keine Einsicht in die Datenschichtimplementierung.
Zum Beispiel ist es möglich, dass ein großer Fehler vorliegt, bei dem die Datenschicht den Cache vollständig überspringt und die Daten immer aus der Datenbank abruft. Die Tests werden bestanden, aber wir können nicht vom Cache profitieren. In Teil 5 werde ich über das Testen Ihres Cache sprechen, was sehr wichtig ist.
In diesem Lernprogramm wurde das Testen mit einer lokalen komplexen Datenschicht beschrieben, die aus mehreren Datenspeichern besteht (relationale Datenbank und Redis-Cache). Wir haben Docker auch verwendet, um mehrere Datenspeicher zum Testen einfach bereitzustellen.
In Teil vier werden wir uns auf Tests mit entfernten Datenspeichern konzentrieren, Snapshots von Produktionsdaten für unsere Tests verwenden und auch unsere eigenen Testdaten generieren. Bleib dran!