Dzisiaj wykonamy ostatni projekt czwartego modułu JRU. Co to będzie? Spróbujmy pracować z różnymi technologiami: MySQL, Hibernate, Redis, Docker. Teraz więcej tematu.

Zadanie: mamy relacyjną bazę danych MySQL ze schematem (kraj-miasto, język według kraju). I jest częsta prośba miasta, które zwalnia. Znaleźliśmy rozwiązanie - przenieść wszystkie często żądane dane do Redis (w pamięci typu klucz-wartość).

I nie potrzebujemy wszystkich danych, które są przechowywane w MySQL, a tylko wybrany zestaw pól. Projekt będzie miał formę tutorialu. Oznacza to, że tutaj poruszymy problem i natychmiast go rozwiążemy.

Zacznijmy więc od tego, jakiego oprogramowania będziemy potrzebować:

  1. IDEA Ultimate (któremu zabrakło klucza - napisz do Romana w Slacku)
  2. Workbench (lub dowolny inny klient dla MySQL)
  3. Doker
  4. wgląd w redis — opcjonalnie

Nasz plan działania:

  1. Skonfiguruj dockera (nie będę tego robił w tutorialu, ponieważ każdy system operacyjny będzie miał swoje własne cechy, a w Internecie jest mnóstwo odpowiedzi na pytania typu „jak zainstalować dockera na Windowsie”), sprawdź, czy wszystko działa.
  2. Uruchom serwer MySQL jako kontener dokera.
  3. Rozwiń zrzut .
  4. Utwórz projekt w Idea, dodaj zależności maven.
  5. Utwórz domenę warstwy.
  6. Napisz metodę pobierania wszystkich danych z MySQL.
  7. Napisz metodę transformacji danych (w Redis zapiszemy tylko te dane, które są często wymagane).
  8. Uruchom serwer Redis jako kontener dokera.
  9. Zapisz dane do Redis.
  10. Opcjonalnie: zainstaluj redis-insight, przejrzyj dane przechowywane w Redis.
  11. Napisz metodę pobierania danych z Redis.
  12. Napisz metodę pobierania danych z MySQL.
  13. Porównaj szybkość uzyskiwania tych samych danych z MySQL i Redis.

Konfiguracja Dockera

Docker to otwarta platforma do tworzenia, dostarczania i obsługi aplikacji. Wykorzystamy go, aby nie instalować i konfigurować Redisa na lokalnej maszynie, ale aby skorzystać z gotowego obrazu. Możesz przeczytać więcej o dockerze tutaj lub zobaczyć tutaj . Jeśli nie jesteś zaznajomiony z dokerem, polecam spojrzeć tylko na drugi link.

Aby upewnić się, że masz zainstalowanego i skonfigurowanego dockera, uruchom polecenie:docker -v

Jeśli wszystko jest w porządku, zobaczysz wersję dockera

Uruchom serwer MySQL jako kontener dokera

Aby móc porównać czas zwrotu danych z MySQL i Redis, w dockerze wykorzystamy również MySQL. W PowerShell (lub innym terminalu konsoli, jeśli nie używasz systemu Windows) uruchom polecenie:

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

Zastanów się, co robimy z tym poleceniem:

  • docker run– uruchomienie (i pobranie, jeśli nie zostało jeszcze pobrane na lokalną maszynę) obrazu. W wyniku uruchomienia otrzymujemy działający kontener.
  • --name mysql- ustaw nazwę kontenera mysql.
  • -d- flaga, która mówi, że kontener powinien nadal działać, nawet jeśli zamkniesz okno terminala, z którego ten kontener został uruchomiony.
  • -p 3306:3306- określa porty. Przed dwukropkiem - port na lokalnej maszynie, po dwukropku - port w kontenerze.
  • -e MYSQL_ROOT_PASSWORD=root– przekazanie do kontenera zmiennej środowiskowej MYSQL_ROOT_PASSWORD z wartością root. Flaga specyficzna dla mysql/image
  • --restart unless-stopped- ustawienie polityki zachowania (czy kontener ma być restartowany po zamknięciu). Wartość „chyba że zatrzymana” oznacza, że ​​zawsze należy uruchamiać ponownie, z wyjątkiem sytuacji, gdy kontener został zatrzymany /
  • -v mysql:/var/lib/mysql – dodaj głośność (obraz do przechowywania informacji).
  • mysql:8 – nazwa obrazu i jego wersja.

Po wykonaniu polecenia w terminalu doker pobierze wszystkie warstwy obrazu i uruchomi kontener:

Ważna uwaga: jeśli masz zainstalowany MySQL jako usługę na komputerze lokalnym i jest on uruchomiony, musisz określić inny port w poleceniu startowym lub zatrzymać tę działającą usługę.

Rozwiń zrzut

Aby rozszerzyć zrzut, musisz utworzyć nowe połączenie z bazą danych z Workbencha, w którym określasz parametry. Użyłem domyślnego portu (3306), nie zmieniłem nazwy użytkownika (domyślnie root) i ustawiłem hasło dla użytkownika root (root).

W Workbench zrób Data Import/Restorei wybierz Import from Self Contained File. Określ, skąd pobrałeś zrzut jako plik . Nie musisz wcześniej tworzyć schematu - jego tworzenie jest zawarte w pliku zrzutu. Po udanym imporcie będziesz mieć światowy schemat z trzema tabelami:

  1. miasto to tablica miast.
  2. kraj - tabela krajów.
  3. kraj_język - tabela, która wskazuje, jaki procent populacji w danym kraju posługuje się danym językiem.

Ponieważ podczas uruchamiania kontenera użyliśmy woluminu, po zatrzymaniu, a nawet usunięciu kontenera mysql i ponownym wykonaniu polecenia start ( docker run --name mysql -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root --restart unless-stopped -v mysql:/var/lib/mysql mysql:8), nie będzie potrzeby ponownego wdrażania zrzutu - jest on już wdrożony w wolumenie.

Utwórz projekt w Idea, dodaj zależności maven

Wiesz już jak stworzyć projekt w Idei - to najłatwiejszy punkt w dzisiejszym projekcie.

Dodaj zależności do pliku pom:


<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> 

Pierwsze trzy zależności są ci znane od dawna.

lettuce-corejest jednym z dostępnych klientów Java do pracy z Redis.

jackson-databind– zależność do wykorzystania ObjectMapper (do transformacji danych do przechowywania w Redis (klucz-wartość typu String)).

Również w folderze zasobów (src/main/resources) dodaj spy.properties, aby wyświetlić żądania z parametrami wykonywanymi przez Hibernate. Zawartość pliku:

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 

Utwórz domenę warstwy

Utwórz pakiet com.codegym.domain

Jest dla mnie wygodne, gdy mapuję tabele na encję, aby użyć struktury tabeli w Idei, więc dodajmy połączenie z bazą danych w Idei.

Sugeruję tworzenie podmiotów w następującej kolejności:

  • Kraj
  • miasto
  • Krajowy język

Pożądane jest samodzielne wykonanie mapowania.

Kod klasy kraju:

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

}

W kodzie są 3 interesujące punkty.

Pierwszy to Kontynent enam , który jest przechowywany w bazie danych jako wartości porządkowe. W strukturze tabeli krajów, w komentarzach do pola kontynentu, widać, która wartość liczbowa odpowiada jakiemu kontynentowi.

package com.codegym.domain;

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

Drugi punkt to zbiór jednostekCountryLanguage . Oto łącze @OneToMany, którego nie było w drugiej wersji roboczej tego modułu. Domyślnie Hibernate nie pobierze wartości tego zestawu podczas żądania jednostki kraju. Ale ponieważ musimy odjąć wszystkie wartości od relacyjnej bazy danych do buforowania, plik FetchType.EAGER.

Trzecie pole to miasto . Komunikacja @OneToOne- jakby wszystko było znajome i zrozumiałe. Ale jeśli spojrzymy na strukturę klucza obcego w bazie danych, zobaczymy, że kraj (kraj) ma powiązanie ze stolicą (miastem), a miasto (miasto) ma powiązanie z krajem (krajem). Istnieje cykliczna zależność.

Na razie nic z tym nie zrobimy, ale kiedy przejdziemy do pozycji „Napisz metodę pobierania wszystkich danych z MySQL”, zobaczmy, jakie zapytania wykonuje Hibernate, spójrzmy na ich liczbę i zapamiętajmy ten element.

Kod klasy miasta:

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

}

Kod klasy CountryLanguage:

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
}

Napisz metodę pobierania wszystkich danych z MySQL

W klasie Main deklarujemy pola:

private final SessionFactory sessionFactory;
private final RedisClient redisClient;

private final ObjectMapper mapper;

private final CityDAO cityDAO;
private final CountryDAO countryDAO;

i zainicjuj je w konstruktorze klasy Main:

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

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

Jak widać metod i klas jest za mało - napiszmy je.

Zadeklaruj pakiet com.codegym.dao i dodaj do niego 2 klasy:

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());
    }
}

Teraz możesz zaimportować te 2 klasy do Main. Nadal brakuje dwóch metod:

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;
}

Nie dotarliśmy jeszcze do rzodkiewki, więc implementacja inicjalizacji klienta rzodkiewki pozostanie na razie skrótem:

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

Na koniec możemy napisać metodę, w której wyciągamy wszystkie miasta:

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;
    }
}

Funkcja implementacji jest taka, że ​​otrzymujemy po 500 miast. Jest to konieczne, ponieważ istnieją ograniczenia co do ilości przesyłanych danych. Tak, w naszym przypadku nie dotrzemy do nich, bo. w bazie mamy łącznie 4079 miast. Ale w aplikacjach produkcyjnych, gdy trzeba uzyskać dużo danych, ta technika jest często używana.

I implementacja głównej metody:

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

Teraz możemy po raz pierwszy uruchomić naszą aplikację w trybie debugowania i zobaczyć, jak działa (lub nie działa - tak, często się to zdarza).

Miasta stają się. Każde miasto otrzymuje kraj, jeśli nie został on wcześniej odjęty z bazy danych dla innego miasta. Obliczmy z grubsza, ile zapytań Hibernate wyśle ​​do bazy danych:

  • 1 prośba o sprawdzenie całkowitej liczby miast (potrzebna jest iteracja ponad 500 miast, aby wiedzieć, kiedy się zatrzymać).
  • 4079 / 500 = 9 zapytań (lista miast).
  • Każde miasto otrzymuje kraj, jeśli nie został on wcześniej odjęty. Ponieważ w bazie danych znajduje się 239 krajów, otrzymamy 239 zapytań.

Total 249 requests. I powiedzieliśmy też, że razem z krajem powinniśmy natychmiast otrzymać zestaw języków, inaczej zapanowałaby ogólna ciemność. Ale to wciąż dużo, więc zmodyfikujmy trochę zachowanie. Zacznijmy od refleksji: co robić, gdzie biec? Ale tak na poważnie - dlaczego jest tyle próśb. Jeśli spojrzysz na dziennik żądań, zobaczysz, że każdy kraj jest wymagany osobno, więc pierwsze proste rozwiązanie: poprośmy wszystkie kraje razem, ponieważ wiemy z góry, że będziemy potrzebować ich wszystkich w tej transakcji.

W metodzie fetchData() zaraz po rozpoczęciu transakcji dodaj następującą linię:

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

Liczymy zgłoszenia:

  • 1 - zdobądź wszystkie kraje
  • 239 - zapytanie o każdy kraj ze swoją stolicą
  • 1 - zapytanie o ilość miast
  • 9 - prośba o listy miast

Total 250. Pomysł jest dobry, ale nie zadziałał. Problem polega na tym, że kraj ma połączenie ze stolicą (miastem) @OneToOne. I taki link jest ładowany domyślnie od razu ( FetchType.EAGER). Załóżmy FetchType.LAZY, ponieważ w każdym razie załadujemy wszystkie miasta później w tej samej transakcji.

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

Stolice nie są już wymagane osobno, ale liczba żądań nie uległa zmianie. Teraz dla każdego kraju lista CountryLanguage jest wymagana przez osobne zapytanie . To znaczy, że jest postęp i idziemy w dobrym kierunku. Jak pamiętacie, na wykładach sugerowano rozwiązanie „join fetch” , aby w jednym żądaniu zażądać podmiotu z danymi zależnymi, dodając do żądania dodatkowe sprzężenie. W CountryDAO przepisz zapytanie HQL w metodzie getAll()na:

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

Początek. Patrzymy na dziennik, liczymy żądania:

  • 1 - wszystkie kraje z językami
  • 1 - liczba miast
  • 9 - wykazy miast.

Total 11- udało się)) Jeśli nie tylko przeczytałeś cały ten tekst, ale także próbowałeś go uruchomić po każdym kroku dostrajania aplikacji, powinieneś nawet wizualnie odnotować kilkakrotne przyspieszenie całej aplikacji.

Napisz metodę transformacji danych

Stwórzmy pakiet, com.codegym.redisw którym dodamy 2 klasy: CityCountry (dane o mieście i kraju, w którym to miasto się znajduje) oraz Language (dane o języku). Oto wszystkie pola, które są często wymagane „według zadania” w „żądaniu hamowania”.

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
}

W metodzie main, po zdobyciu wszystkich miast, dodaj linię

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

I zaimplementuj tę metodę:

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());
}

Myślę, że ta metoda jest oczywista: po prostu tworzymy jednostkę CityCountry i wypełniamy ją danymi z City , Country , CountryLanguage .

Uruchom serwer Redis jako kontener dokera

Tutaj są 2 opcje. Jeśli wykonasz opcjonalny krok „zainstaluj redis-insight, spójrz na dane przechowywane w Redis”, to polecenie jest dla Ciebie:

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

Jeśli zdecydujesz się pominąć ten krok, po prostu:

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

Różnica polega na tym, że w pierwszej opcji port 8001 jest przekazywany do lokalnej maszyny, z którą można połączyć się z zewnętrznym klientem, aby zobaczyć, co jest w niej przechowywane. I dlatego nadawałem znaczące nazwy redis-stacklub redis.

Po uruchomieniu możesz zobaczyć listę uruchomionych kontenerów. Aby to zrobić, uruchom polecenie:

docker container ls 

I zobaczysz coś takiego:

Jeśli potrzebujesz znaleźć jakieś polecenie, możesz zajrzeć do pomocy terminala (pomoc dokera) lub google „jak ...” (na przykład doker, jak usunąć działający kontener).

Wywołaliśmy również inicjalizację klienta Radish w konstruktorze głównym, ale nie zaimplementowaliśmy samej metody. Dodaj implementację:

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 został dodany w celach edukacyjnych, aby w dzienniku uruchamiania można było zobaczyć, że wszystko jest w porządku, a połączenie przez klienta rzodkiewki przeszło bez błędów.

Zapisz dane do Redis

Dodaj wywołanie do metody main

main.pushToRedis(preparedData); 

Dzięki tej implementacji metody:

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();
            }
        }

    }
}

Tutaj otwierane jest synchroniczne połączenie z klientem Radish i sekwencyjnie każdy obiekt typu CityCountry jest zapisywany w Radish. Ponieważ rzodkiewka jest magazynem klucz-wartość typu String , klucz (identyfikator miasta) jest konwertowany na ciąg znaków. Wartość dotyczy również łańcucha, ale przy użyciu ObjectMapper w formacie JSON.

Pozostaje uruchomić i sprawdzić, czy w dzienniku nie ma błędów. Wszystko działało.

Zainstaluj redis-insight, spójrz na dane przechowywane w Redis (opcjonalnie)

Pobierz redis-insight z łącza i zainstaluj go. Po uruchomieniu od razu pokazuje naszą instancję rzodkiewki w kontenerze dokera:

Jeśli się zalogujesz, zobaczymy listę wszystkich kluczy:

I możesz przejść do dowolnego klucza, aby zobaczyć, jakie dane są na nim przechowywane:

Napisz metodę pobierania danych z Redis

Do testowania używamy następującego testu: otrzymujemy 10 rekordów CityCountry. Każdy z osobnym żądaniem, ale w jednym połączeniu.

Dane z rzodkiewki można uzyskać za pośrednictwem naszego klienta rzodkiewki. Aby to zrobić, napiszmy metodę, która pobiera listę identyfikatorów do pobrania.

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();
            }
        }
    }
}

Implementacja, jak sądzę, jest intuicyjna: otwieramy połączenie synchroniczne, a dla każdego id otrzymujemy JSON String , który konwertujemy na obiekt typu CityCountry , którego potrzebujemy .

Napisz metodę pobierania danych z MySQL

W klasie CityDAO dodaj metodę getById(Integer id), w której otrzymamy miasto wraz z krajem:

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();
}

Analogicznie do poprzedniego akapitu dodajmy podobną metodę dla MySQL do klasy Main:

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();
    }
}

Spośród funkcji, aby mieć pewność uzyskania pełnego obiektu (bez pośredników proxy), wyraźnie prosimy o listę języków z kraju.

Porównaj szybkość uzyskiwania tych samych danych z MySQL i Redis

Tutaj od razu podam kod głównej metody oraz wynik, który uzyskano na moim lokalnym komputerze.

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();
}

Podczas testowania istnieje funkcja - dane z MySQL są tylko odczytywane, więc nie można go ponownie uruchomić między uruchomieniami naszej aplikacji. A w Redis są napisane.

Chociaż przy próbie dodania duplikatu dla tego samego klucza dane zostaną po prostu zaktualizowane, zalecałbym uruchamianie komend zatrzymujących kontener docker stop redis-stacki usuwających kontener między uruchomieniami aplikacji w terminalu docker rm redis-stack. Następnie ponownie podnieś pojemnik z rzodkiewką docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:latesti dopiero wtedy wykonaj naszą aplikację.

Oto wyniki moich testów:

W sumie osiągnęliśmy półtorakrotny wzrost wydajności odpowiedzi na żądanie „częstego hamowania”. I to biorąc pod uwagę fakt, że w testach użyliśmy nie najszybszej deserializacji poprzez ObjectMapper . Jeśli zmienisz go na GSON, najprawdopodobniej możesz „wygrać” trochę więcej czasu.

W tej chwili przypomina mi się dowcip o programiście i czasie: przeczytaj i pomyśl, jak napisać i zoptymalizować swój kod.