Heute werden wir das Abschlussprojekt zum vierten JRU-Modul durchführen. Was wird es sein? Versuchen wir, mit verschiedenen Technologien zu arbeiten: MySQL, Hibernate, Redis, Docker. Jetzt mehr Thema.

Aufgabe: Wir haben eine relationale MySQL-Datenbank mit einem Schema (Land-Stadt, Sprache nach Land). Und es gibt eine häufige Anfrage der Stadt, die langsamer wird. Wir haben eine Lösung gefunden – alle häufig angeforderten Daten nach Redis zu verschieben (im Speicher vom Typ Schlüsselwert).

Und wir brauchen nicht alle Daten, die in MySQL gespeichert sind, sondern nur einen ausgewählten Satz von Feldern. Das Projekt wird in Form eines Tutorials durchgeführt. Das heißt, hier werden wir das Problem ansprechen und es sofort lösen.

Beginnen wir also mit der Software, die wir benötigen:

  1. IDEA Ultimate (Wem der Schlüssel ausgegangen ist – schreiben Sie Roman im Slack)
  2. Workbench (oder ein anderer Client für MySQL)
  3. Docker
  4. redis-insight – optional

Unser Aktionsplan:

  1. Richten Sie Docker ein (ich werde dies im Tutorial nicht tun, da jedes Betriebssystem seine eigenen Eigenschaften hat und es im Internet viele Antworten auf Fragen wie „Wie installiert man Docker unter Windows“) gibt, prüfen Sie, ob alles funktioniert.
  2. Führen Sie den MySQL-Server als Docker-Container aus.
  3. Dump erweitern .
  4. Erstellen Sie ein Projekt in Idea und fügen Sie Maven-Abhängigkeiten hinzu.
  5. Ebenendomäne erstellen.
  6. Schreiben Sie eine Methode, um alle Daten von MySQL abzurufen.
  7. Schreiben Sie eine Datentransformationsmethode (in Redis schreiben wir nur die Daten, die häufig angefordert werden).
  8. Führen Sie den Redis-Server als Docker-Container aus.
  9. Daten in Redis schreiben.
  10. Optional: Installieren Sie redis-insight und sehen Sie sich die in Redis gespeicherten Daten an.
  11. Schreiben Sie eine Methode zum Abrufen von Daten von Redis.
  12. Schreiben Sie eine Methode zum Abrufen von Daten von MySQL.
  13. Vergleichen Sie die Geschwindigkeit, mit der dieselben Daten von MySQL und Redis abgerufen werden.

Docker-Setup

Docker ist eine offene Plattform zum Entwickeln, Bereitstellen und Betreiben von Anwendungen. Wir werden es verwenden, um Redis nicht auf dem lokalen Computer zu installieren und zu konfigurieren, sondern um ein vorgefertigtes Image zu verwenden. Mehr über Docker können Sie hier lesen oder hier sehen . Wenn Sie mit Docker nicht vertraut sind, empfehle ich Ihnen, sich nur den zweiten Link anzusehen.

Um sicherzustellen, dass Docker installiert und konfiguriert ist, führen Sie den folgenden Befehl aus:docker -v

Wenn alles in Ordnung ist, sehen Sie die Docker-Version

Führen Sie den MySQL-Server als Docker-Container aus

Um den Zeitpunkt der Datenrückgabe von MySQL und Redis vergleichen zu können, werden wir auch MySQL im Docker verwenden. Führen Sie in PowerShell (oder einem anderen Konsolenterminal, wenn Sie nicht Windows verwenden) den folgenden Befehl aus:

docker run --name mysql -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root --restart unless-stopped -v mysql:/var/lib/mysql mysql:8 

Überlegen Sie, was wir mit diesem Befehl machen:

  • docker run– Starten (und Herunterladen, falls es noch nicht auf den lokalen Computer heruntergeladen wurde) des Bildes. Als Ergebnis des Starts erhalten wir einen laufenden Container.
  • --name mysql- Legen Sie den Namen des MySQL-Containers fest.
  • -d– ein Flag, das besagt, dass der Container weiterhin funktionieren soll, auch wenn Sie das Terminalfenster schließen, von dem aus dieser Container gestartet wurde.
  • -p 3306:3306- gibt Ports an. Vor dem Doppelpunkt – der Port auf dem lokalen Computer, nach dem Doppelpunkt – der Port im Container.
  • -e MYSQL_ROOT_PASSWORD=root– Übergabe der Umgebungsvariablen MYSQL_ROOT_PASSWORD mit dem Wert root an den Container. Flag speziell für das MySQL/-Image
  • --restart unless-stopped- Festlegen der Verhaltensrichtlinie (ob der Container beim Schließen neu gestartet werden soll). Der Wert „unsere-stopped“ bedeutet, dass immer neu gestartet wird, außer wenn der Container gestoppt wurde/
  • -v mysql:/var/lib/mysql – Volumen hinzufügen (Bild zum Speichern von Informationen).
  • mysql:8 – der Name des Bildes und seine Version.

Nachdem der Befehl im Terminal ausgeführt wurde, lädt der Docker alle Ebenen des Bildes herunter und startet den Container:

Wichtiger Hinweis: Wenn MySQL als Dienst auf Ihrem lokalen Computer installiert ist und dieser ausgeführt wird, müssen Sie im Startbefehl einen anderen Port angeben oder diesen laufenden Dienst stoppen.

Dump erweitern

Um den Dump zu erweitern, müssen Sie von Workbench aus eine neue Verbindung zur Datenbank erstellen und dort die Parameter angeben. Ich habe den Standardport (3306) verwendet, den Benutzernamen (standardmäßig root) nicht geändert und das Passwort für den Root-Benutzer (root) festgelegt.

Führen Sie in Workbench Folgendes aus Data Import/Restoreund wählen Sie aus Import from Self Contained File. Geben Sie an, wo Sie den Dump als Datei heruntergeladen haben . Sie müssen das Schema nicht vorher erstellen – seine Erstellung ist in der Dump-Datei enthalten. Nach einem erfolgreichen Import verfügen Sie über ein Weltschema mit drei Tabellen:

  1. Stadt ist eine Tabelle mit Städten.
  2. Land - Ländertabelle.
  3. landessprache – eine Tabelle, die angibt, wie viel Prozent der Bevölkerung im Land eine bestimmte Sprache spricht.

Da wir beim Starten des Containers ein Volume verwendet haben, besteht nach dem Stoppen und sogar Löschen des MySQL-Containers und der erneuten Ausführung des Startbefehls ( docker run --name mysql -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root --restart unless-stopped -v mysql:/var/lib/mysql mysql:8) keine Notwendigkeit, den Dump erneut bereitzustellen – er ist bereits im Volume bereitgestellt.

Erstellen Sie ein Projekt in Idea und fügen Sie Maven-Abhängigkeiten hinzu

Sie wissen bereits, wie Sie in der Idee ein Projekt erstellen – dies ist der einfachste Punkt im heutigen Projekt.

Fügen Sie der POM-Datei Abhängigkeiten hinzu:


<dependencies> 
   <dependency> 
      <groupId>mysql</groupId> 
      <artifactId>mysql-connector-java</artifactId> 
      <version>8.0.30</version> 
   </dependency> 
 
   <dependency> 
      <groupId>org.hibernate</groupId> 
      <artifactId>hibernate-core-jakarta</artifactId> 
      <version>5.6.14.Final</version> 
   </dependency> 
 
   <dependency> 
      <groupId>p6spy</groupId> 
      <artifactId>p6spy</artifactId> 
      <version>3.9.1</version> 
   </dependency> 
    
   <dependency> 
      <groupId>io.lettuce</groupId> 
      <artifactId>lettuce-core</artifactId> 
      <version>6.2.2.RELEASE</version> 
   </dependency> 
 
   <dependency> 
      <groupId>com.fasterxml.jackson.core</groupId> 
      <artifactId>jackson-databind</artifactId> 
      <version>2.14.0</version> 
   </dependency> 
</dependencies> 

Die ersten drei Abhängigkeiten sind Ihnen schon lange bekannt.

lettuce-coreist einer der verfügbaren Java-Clients für die Arbeit mit Redis.

jackson-databind– Abhängigkeit für die Verwendung von ObjectMapper (um Daten für die Speicherung in Redis zu transformieren (Schlüsselwert vom Typ String)).

Fügen Sie außerdem im Ressourcenordner (src/main/resources) spy.properties hinzu, um die Anforderungen mit Parametern anzuzeigen, die Hibernate ausführt. Dateiinhalt:

driverlist=com.mysql.cj.jdbc.Driver 
dateformat=yyyy-MM-dd hh:mm:ss a 
appender=com.p6spy.engine.spy.appender.StdoutLogger 
logMessageFormat=com.p6spy.engine.spy.appender.MultiLineFormat 

Ebenendomäne erstellen

Erstellen Sie das Paket com.codegym.domain

Beim Zuordnen von Tabellen zu einer Entität ist es für mich praktisch, die Tabellenstruktur in der Idee zu verwenden. Fügen wir also eine Datenbankverbindung in der Idee hinzu.

Ich schlage vor, Entitäten in dieser Reihenfolge zu erstellen:

  • Land
  • Stadt
  • Landessprache

Es ist wünschenswert, dass Sie das Mapping selbst durchführen.

Länderklassencode:

package com.codegym.domain;

import jakarta.persistence.*;

import java.math.BigDecimal;
import java.util.Set;

@Entity
@Table(schema = "world", name = "country")
public class Country {
    @Id
    @Column(name = "id")
    private Integer id;

    private String code;

    @Column(name = "code_2")
    private String alternativeCode;

    private String name;

    @Column(name = "continent")
    @Enumerated(EnumType.ORDINAL)
    private Continent continent;

    private String region;

    @Column(name = "surface_area")
    private BigDecimal surfaceArea;

    @Column(name = "indep_year")
    private Short independenceYear;

    private Integer population;

    @Column(name = "life_expectancy")
    private BigDecimal lifeExpectancy;

    @Column(name = "gnp")
    private BigDecimal GNP;

    @Column(name = "gnpo_id")
    private BigDecimal GNPOId;

    @Column(name = "local_name")
    private String localName;

    @Column(name = "government_form")
    private String governmentForm;

    @Column(name = "head_of_state")
    private String headOfState;

    @OneToOne
    @JoinColumn(name = "capital")
    private City city;

    @OneToMany(fetch = FetchType.EAGER)
    @JoinColumn(name = "country_id")
    private Set<CountryLanguage> languages;


    //Getters and Setters omitted

}

Der Code enthält drei interessante Punkte.

Der erste ist der Continent enam , der als Ordinalwerte in der Datenbank gespeichert wird. In der Struktur der Ländertabelle können Sie in den Kommentaren zum Feld Kontinent erkennen, welcher Zahlenwert welchem ​​Kontinent entspricht.

package com.codegym.domain;

public enum Continent {
    ASIA,
    EUROPE,
    NORTH_AMERICA,
    AFRICA,
    OCEANIA,
    ANTARCTICA,
    SOUTH_AMERICA
}

Der zweite Punkt ist eine Menge von EntitätenCountryLanguage . Hier ist ein Link @OneToMany, der nicht im zweiten Entwurf dieses Moduls enthalten war. Standardmäßig ruft Hibernate den Wert dieses Satzes nicht ab, wenn eine Länderentität angefordert wird. Da wir aber zum Caching alle Werte von der relationalen Datenbank subtrahieren müssen, ist die FetchType.EAGER.

Das dritte ist das Stadtfeld . Kommunikation @OneToOne– als wäre alles vertraut und verständlich. Wenn wir uns jedoch die Fremdschlüsselstruktur in der Datenbank ansehen, sehen wir, dass das Land (Land) eine Verbindung zur Hauptstadt (Stadt) und die Stadt (Stadt) eine Verbindung zum Land (Land) hat. Es besteht ein zyklischer Zusammenhang.

Wir werden damit noch nichts anfangen, aber wenn wir zum Punkt „Schreiben Sie eine Methode zum Abrufen aller Daten von MySQL“ kommen, schauen wir uns an, welche Abfragen Hibernate ausführt, sehen uns deren Anzahl an und merken uns diesen Punkt. Hibernate

Stadtklassencode:

package com.codegym.domain;

import jakarta.persistence.*;

@Entity
@Table(schema = "world", name = "city")
public class City {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String name;

    @ManyToOne
    @JoinColumn(name = "country_id")
    private Country country;

    private String district;

    private Integer population;


    //Getters and Setters omitted

}

CountryLanguage-Klassencode:

package com.codegym.domain;

import jakarta.persistence.*;
import org.hibernate.annotations.Type;

import java.math.BigDecimal;

@Entity
@Table(schema = "world", name = "country_language")
public class CountryLanguage {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Integer id;

    @ManyToOne
    @JoinColumn(name = "country_id")
    private Country country;

    private String language;

    @Column(name = "is_official", columnDefinition = "BIT")
    @Type(type = "org.hibernate.type.NumericBooleanType")
    private Boolean isOfficial;

    private BigDecimal percentage;


    //Getters and Setters omitted
}

Schreiben Sie eine Methode, um alle Daten von MySQL abzurufen

In der Main-Klasse deklarieren wir die Felder:

private final SessionFactory sessionFactory;
private final RedisClient redisClient;

private final ObjectMapper mapper;

private final CityDAO cityDAO;
private final CountryDAO countryDAO;

und initialisieren Sie sie im Konstruktor der Main-Klasse:

public Main() {
    sessionFactory = prepareRelationalDb();
    cityDAO = new CityDAO(sessionFactory);
    countryDAO = new CountryDAO(sessionFactory);

    redisClient = prepareRedisClient();
    mapper = new ObjectMapper();
}

Wie Sie sehen, gibt es nicht genügend Methoden und Klassen – schreiben wir sie.

Deklarieren Sie ein Paket com.codegym.dao und fügen Sie ihm zwei Klassen hinzu:

package com.codegym.dao;

import com.codegym.domain.Country;
import org.hibernate.SessionFactory;
import org.hibernate.query.Query;

import java.util.List;

public class CountryDAO {
    private final SessionFactory sessionFactory;

    public CountryDAO(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    public List<Country> getAll() {
        Query<Country> query = sessionFactory.getCurrentSession().createQuery("select c from Country c", Country.class);
        return query.list();
    }
}
package com.codegym.dao;

import com.codegym.domain.City;
import org.hibernate.SessionFactory;
import org.hibernate.query.Query;

import java.util.List;

public class CityDAO {
    private final SessionFactory sessionFactory;

    public CityDAO(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    public List<City> getItems(int offset, int limit) {
        Query<City> query = sessionFactory.getCurrentSession().createQuery("select c from City c", City.class);
        query.setFirstResult(offset);
        query.setMaxResults(limit);
        return query.list();
    }

    public int getTotalCount() {
        Query<Long> query = sessionFactory.getCurrentSession().createQuery("select count(c) from City c", Long.class);
        return Math.toIntExact(query.uniqueResult());
    }
}

Jetzt können Sie diese beiden Klassen in Main importieren. Es fehlen noch zwei Methoden:

private SessionFactory prepareRelationalDb() {
    final SessionFactory sessionFactory;
    Properties properties = new Properties();
    properties.put(Environment.DIALECT, "org.hibernate.dialect.MySQL8Dialect");
    properties.put(Environment.DRIVER, "com.p6spy.engine.spy.P6SpyDriver");
    properties.put(Environment.URL, "jdbc:p6spy:mysql://localhost:3306/world");
    properties.put(Environment.USER, "root");
    properties.put(Environment.PASS, "root");
    properties.put(Environment.CURRENT_SESSION_CONTEXT_CLASS, "thread");
    properties.put(Environment.HBM2DDL_AUTO, "validate");
    properties.put(Environment.STATEMENT_BATCH_SIZE, "100");

    sessionFactory = new Configuration()
            .addAnnotatedClass(City.class)
            .addAnnotatedClass(Country.class)
            .addAnnotatedClass(CountryLanguage.class)
            .addProperties(properties)
            .buildSessionFactory();
    return sessionFactory;
}

Da wir noch nicht beim Radieschen angelangt sind, wird die Implementierung der Initialisierung des Radieschen-Clients vorerst ein Stub bleiben:

private void shutdown() {
    if (nonNull(sessionFactory)) {
        sessionFactory.close();
    }
    if (nonNull(redisClient)) {
        redisClient.shutdown();
    }
}

Schließlich können wir eine Methode schreiben, mit der wir alle Städte herausziehen:

private List<City> fetchData(Main main) {
    try (Session session = main.sessionFactory.getCurrentSession()) {
        List<City> allCities = new ArrayList<>();
        session.beginTransaction();

        int totalCount = main.cityDAO.getTotalCount();
        int step = 500;
        for (int i = 0; i < totalCount; i += step) {
            allCities.addAll(main.cityDAO.getItems(i, step));
        }
        session.getTransaction().commit();
        return allCities;
    }
}

Die Implementierungsfunktion ist so, dass wir jeweils 500 Städte erhalten. Dies ist notwendig, da es Beschränkungen hinsichtlich der Menge der übertragenen Daten gibt. Ja, in unserem Fall werden wir sie nicht erreichen, weil. wir haben insgesamt 4079 Städte in der Datenbank. Aber in Produktionsanwendungen, wenn Sie viele Daten benötigen, wird diese Technik häufig verwendet.

Und die Implementierung der Hauptmethode:

public static void main(String[] args) {
    Main main = new Main();
    List<City> allCities = main.fetchData(main);
    main.shutdown();
}

Jetzt können wir unsere Anwendung zum ersten Mal im Debug-Modus ausführen und sehen, wie sie funktioniert (oder nicht funktioniert – ja, das kommt oft vor).

Städte bekommen. Jede Stadt erhält ein Land, sofern sie nicht zuvor für eine andere Stadt aus der Datenbank abgezogen wurde. Berechnen wir grob, wie viele Abfragen Hibernate an die Datenbank sendet:

  • 1 Anfrage, um die Gesamtzahl der Städte herauszufinden (es ist erforderlich, über 500 Städte zu durchlaufen, um zu wissen, wann man aufhören muss).
  • 4079 / 500 = 9 Anfragen (Liste der Städte).
  • Jede Stadt bekommt ein Land, sofern sie nicht schon vorher abgezogen wurde. Da die Datenbank 239 Länder enthält, ergeben sich daraus 239 Abfragen.

Total 249 requests. Und wir haben auch gesagt, dass wir zusammen mit dem Land sofort eine Reihe von Sprachen erhalten sollten, sonst würde es allgemein Dunkelheit geben. Aber es ist immer noch viel, also lasst uns das Verhalten ein wenig optimieren. Beginnen wir mit Überlegungen: Was tun, wohin laufen? Aber im Ernst – warum gibt es so viele Anfragen? Wenn wir uns das Anfrageprotokoll ansehen, sehen wir, dass jedes Land separat angefragt wird. Daher die erste einfache Lösung: Lassen Sie uns alle Länder gemeinsam anfragen, da wir im Voraus wissen, dass wir sie alle in dieser Transaktion benötigen.

Fügen Sie in der fetchData()-Methode unmittelbar nach dem Start der Transaktion die folgende Zeile hinzu:

List<Country> countries = main.countryDAO.getAll(); 

Wir zählen Anfragen:

  • 1 – alle Länder abrufen
  • 239 – Abfrage für jedes Land mit seiner Hauptstadt
  • 1 - Anfrage nach der Anzahl der Städte
  • 9 - Anfrage nach Städtelisten

Total 250. Die Idee ist gut, aber sie hat nicht funktioniert. Das Problem besteht darin, dass das Land eine Verbindung zur Hauptstadt (Stadt) hat @OneToOne. Und ein solcher Link wird standardmäßig sofort geladen ( FetchType.EAGER). Sagen wir FetchType.LAZY, weil Wie auch immer, wir werden alle Städte später in derselben Transaktion laden.

@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "capital")
private City city;

Großbuchstaben werden nicht mehr separat abgefragt, die Anzahl der Anträge hat sich jedoch nicht geändert. Nun wird für jedes Land die CountryLanguage- Liste durch eine separate Abfrage angefordert . Das heißt, es gibt Fortschritte und wir bewegen uns in die richtige Richtung. Wenn Sie sich erinnern, wurde in den Vorträgen die „Join-Fetch“ -Lösung vorgeschlagen , um eine Entität mit abhängigen Daten in einer Anfrage anzufordern, indem der Anfrage ein zusätzlicher Join hinzugefügt wird. Schreiben Sie in CountryDAO die HQL-Abfrage in der Methode getAll()um:

"select c from Country c join fetch c.languages" 

Start. Wir schauen uns das Protokoll an und zählen die Anfragen:

  • 1 – alle Länder mit Sprachen
  • 1 - Anzahl der Städte
  • 9 - Städtelisten.

Total 11- es ist uns gelungen)) Wenn Sie diesen gesamten Text nicht nur gelesen, sondern auch versucht haben, ihn nach jedem Schritt der Optimierung der Anwendung auszuführen, sollten Sie die Beschleunigung der gesamten Anwendung sogar mehrmals visuell bemerken.

Schreiben Sie eine Datentransformationsmethode

Erstellen wir ein Paket com.codegym.redis, in dem wir zwei Klassen hinzufügen: CityCountry (Daten zur Stadt und dem Land, in dem sich diese Stadt befindet) und Language (Daten zur Sprache). Hier finden Sie alle Felder, die häufig „aufgabenweise“ in der „Bremsanforderung“ abgefragt werden.

package com.codegym.redis;

import com.codegym.domain.Continent;

import java.math.BigDecimal;
import java.util.Set;

public class CityCountry {
    private Integer id;

    private String name;

    private String district;

    private Integer population;

    private String countryCode;

    private String alternativeCountryCode;

    private String countryName;

    private Continent continent;

    private String countryRegion;

    private BigDecimal countrySurfaceArea;

    private Integer countryPopulation;

    private Set<Language> languages;

    //Getters and Setters omitted
}
package com.codegym.redis;

import java.math.BigDecimal;

public class Language {
    private String language;
    private Boolean isOfficial;
    private BigDecimal percentage;

    //Getters and Setters omitted
}

Fügen Sie in der Hauptmethode die Zeile hinzu, nachdem Sie alle Städte ermittelt haben

List<CityCountry>> preparedData = main.transformData(allCities); 

Und implementieren Sie diese Methode:

private List<CityCountry> transformData(List<City> cities) {
    return cities.stream().map(city -> {
        CityCountry res = new CityCountry();
        res.setId(city.getId());
        res.setName(city.getName());
        res.setPopulation(city.getPopulation());
        res.setDistrict(city.getDistrict());

        Country country = city.getCountry();
        res.setAlternativeCountryCode(country.getAlternativeCode());
        res.setContinent(country.getContinent());
        res.setCountryCode(country.getCode());
        res.setCountryName(country.getName());
        res.setCountryPopulation(country.getPopulation());
        res.setCountryRegion(country.getRegion());
        res.setCountrySurfaceArea(country.getSurfaceArea());
        Set<CountryLanguage> countryLanguages = country.getLanguages();
        Set<Language> languages = countryLanguages.stream().map(cl -> {
            Language language = new Language();
            language.setLanguage(cl.getLanguage());
            language.setOfficial(cl.getOfficial());
            language.setPercentage(cl.getPercentage());
            return language;
        }).collect(Collectors.toSet());
        res.setLanguages(languages);

        return res;
    }).collect(Collectors.toList());
}

Ich denke, diese Methode ist selbsterklärend: Wir erstellen einfach eine CityCountry- Entität und füllen sie mit Daten aus City , Country und CountryLanguage .

Führen Sie den Redis-Server als Docker-Container aus

Hier gibt es 2 Möglichkeiten. Wenn Sie den optionalen Schritt „Redis-Insight installieren, sich die in Redis gespeicherten Daten ansehen“ ausführen, ist der Befehl genau das Richtige für Sie:

docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:latest 

Wenn Sie diesen Schritt überspringen möchten, gehen Sie wie folgt vor:

docker run -d --name redis -p 6379:6379 redis:latest 

Der Unterschied besteht darin, dass bei der ersten Option Port 8001 an den lokalen Computer weitergeleitet wird, zu dem Sie eine Verbindung mit einem externen Client herstellen können, um zu sehen, was darin gespeichert ist. Und ich habe daher immer aussagekräftige Namen gegeben, redis-stackoder redis.

Nach dem Start können Sie die Liste der ausgeführten Container sehen. Führen Sie dazu den Befehl aus:

docker container ls 

Und Sie werden so etwas sehen:

Wenn Sie einen Befehl finden müssen, können Sie entweder in der Terminal-Hilfe (Docker-Hilfe) nachsehen oder „How to ...“ bei Google suchen (z. B. „Docker How to Remove Running Container“).

Außerdem haben wir die Initialisierung des Rettich-Clients im Hauptkonstruktor aufgerufen, die Methode selbst jedoch nicht implementiert. Implementierung hinzufügen:

private RedisClient prepareRedisClient() {
    RedisClient redisClient = RedisClient.create(RedisURI.create("localhost", 6379));
    try (StatefulRedisConnection<String, String> connection = redisClient.connect()) {
        System.out.println("\nConnected to Redis\n");
    }
    return redisClient;
}

sout wurde zu Bildungszwecken hinzugefügt, damit Sie im Startprotokoll sehen können, dass alles in Ordnung ist und die Verbindung über den Radish-Client fehlerfrei verlief.

Schreiben Sie Daten in Redis

Fügen Sie der Hauptmethode einen Aufruf hinzu

main.pushToRedis(preparedData); 

Mit dieser Methodenimplementierung:

private void pushToRedis(List<CityCountry> data) {
    try (StatefulRedisConnection<String, String> connection = redisClient.connect()) {
        RedisStringCommands<String, String> sync = connection.sync();
        for (CityCountry cityCountry : data) {
            try {
                sync.set(String.valueOf(cityCountry.getId()), mapper.writeValueAsString(cityCountry));
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
        }

    }
}

Hier wird eine synchrone Verbindung mit dem Rettich-Client geöffnet und nacheinander jedes Objekt vom Typ CityCountry in den Rettich geschrieben. Da es sich bei Rettich um einen String -Schlüsselwertspeicher handelt , wird der Schlüssel (Stadt-ID) in einen String konvertiert. Und der Wert bezieht sich ebenfalls auf die Zeichenfolge, verwendet jedoch ObjectMapper im JSON-Format.

Es bleibt noch zu laufen und zu überprüfen, ob das Protokoll keine Fehler enthält. Alles hat funktioniert.

Installieren Sie redis-insight und sehen Sie sich die in Redis gespeicherten Daten an (optional).

Laden Sie redis-insight über den Link herunter und installieren Sie es. Nach dem Start wird sofort unsere Rettich-Instanz im Docker-Container angezeigt:

Wenn Sie sich anmelden, sehen wir eine Liste aller Schlüssel:

Und Sie können zu jedem Schlüssel gehen, um zu sehen, welche Daten darauf gespeichert sind:

Schreiben Sie eine Methode zum Abrufen von Daten von Redis

Zum Testen verwenden wir den folgenden Test: Wir erhalten 10 CityCountry-Datensätze. Jeder mit einer separaten Anfrage, aber in einer Verbindung.

Daten zu Rettich erhalten Sie über unseren Rettich-Client. Schreiben wir dazu eine Methode, die eine Liste von IDs abruft.

private void testRedisData(List<Integer> ids) {
    try (StatefulRedisConnection<String, String> connection = redisClient.connect()) {
        RedisStringCommands<String, String> sync = connection.sync();
        for (Integer id : ids) {
            String value = sync.get(String.valueOf(id));
            try {
                mapper.readValue(value, CityCountry.class);
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
        }
    }
}

Die Implementierung ist meiner Meinung nach intuitiv: Wir öffnen eine synchrone Verbindung und erhalten für jede ID einen JSON-String , den wir in das Objekt des benötigten CityCountry- Typs konvertieren .

Schreiben Sie eine Methode, um Daten von MySQL abzurufen

Fügen Sie in der CityDAO- Klasse eine Methode hinzu getById(Integer id), mit der wir die Stadt zusammen mit dem Land erhalten:

public City getById(Integer id) {
    Query<City> query = sessionFactory.getCurrentSession().createQuery("select c from City c join fetch c.country where c.id = :ID", City.class);
    query.setParameter("ID", id);
    return query.getSingleResult();
}

Analog zum vorherigen Absatz fügen wir der Main-Klasse eine ähnliche Methode für MySQL hinzu:

private void testMysqlData(List<Integer> ids) {
    try (Session session = sessionFactory.getCurrentSession()) {
        session.beginTransaction();
        for (Integer id : ids) {
            City city = cityDAO.getById(id);
            Set<CountryLanguage> languages = city.getCountry().getLanguages();
        }
        session.getTransaction().commit();
    }
}

Um sicherzustellen, dass das vollständige Objekt (ohne Proxy-Stubs) von den Funktionen erhalten wird, fordern wir ausdrücklich eine Liste der Sprachen des Landes an.

Vergleichen Sie die Geschwindigkeit, mit der dieselben Daten von MySQL und Redis abgerufen werden

Hier werde ich sofort den Code der Hauptmethode und das Ergebnis angeben, das auf meinem lokalen Computer erzielt wird.

public static void main(String[] args) {
    Main main = new Main();
    List<City> allCities = main.fetchData(main);
    List<CityCountry> preparedData = main.transformData(allCities);
    main.pushToRedis(preparedData);

    // close the current session in order to make a query to the database for sure, and not to pull data from the cache
    main.sessionFactory.getCurrentSession().close();

    //choose random 10 id cities
    //since we did not handle invalid situations, use the existing id in the database
    List<Integer> ids = List.of(3, 2545, 123, 4, 189, 89, 3458, 1189, 10, 102);

    long startRedis = System.currentTimeMillis();
    main.testRedisData(ids);
    long stopRedis = System.currentTimeMillis();

    long startMysql = System.currentTimeMillis();
    main.testMysqlData(ids);
    long stopMysql = System.currentTimeMillis();

    System.out.printf("%s:\t%d ms\n", "Redis", (stopRedis - startRedis));
    System.out.printf("%s:\t%d ms\n", "MySQL", (stopMysql - startMysql));

    main.shutdown();
}

Beim Testen gibt es eine Funktion: Daten von MySQL werden nur gelesen, sodass sie zwischen den Starts unserer Anwendung nicht neu gestartet werden können. Und in Redis sind sie geschrieben.

Obwohl die Daten einfach aktualisiert werden, wenn Sie versuchen, ein Duplikat für denselben Schlüssel hinzuzufügen, würde ich empfehlen, dass Sie die Befehle ausführen, um den Container zu stoppen und den Container docker stop redis-stackzwischen Anwendungsstarts im Terminal zu löschen docker rm redis-stack. Heben Sie danach den Behälter mit dem Rettich erneut an docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:latestund führen Sie erst danach unsere Anwendung aus.

Hier sind meine Testergebnisse:

Insgesamt haben wir eine Steigerung der Performance der Antwort auf die Anfrage „Bremsfrequenz“ um das Eineinhalbfache erreicht. Und dies berücksichtigt die Tatsache, dass wir beim Testen nicht die schnellste Deserialisierung durch ObjectMapper verwendet haben . Wenn Sie es auf GSON umstellen, können Sie höchstwahrscheinlich etwas mehr Zeit „gewinnen“.

In diesem Moment fällt mir ein Witz über einen Programmierer und seine Zeit ein: Lesen Sie und denken Sie darüber nach, wie Sie Ihren Code schreiben und optimieren.