Сьогодні робитимемо фінальний проект з четвертого модулю JRU. Що це буде? Спробуємо працювати з різними технологіями: MySQL, Hibernate, Redis, Docker. Тепер предметніший.

Завдання: у нас є реляційна база даних MySQL зі схемою (країна-місто, мова по країні). І є частий запит міста, яке гальмує. Ми придумали рішення – винести всі дані, які запитуються часто, у Redis (in memory storage типу ключ-значення).

І потрібні нам не всі дані, які зберігаються в MySQL, а тільки обраний набір полів. Проект буде у вигляді туторіалу. Тобто тут підніматимемо проблему і відразу ж її вирішуватимемо.

Отже, почнемо з того, який софт нам буде потрібний:

  1. IDEA Ultimate (у кого закінчився ключ – пишіть у слаці Роману)
  2. Workbench (або будь-який інший клієнт для MySQL)
  3. Docker
  4. redis-insight – опціонально

Наш план дій:

  1. Налаштувати докер (у туторіалі це не робитиму, тому що для кожної ОС будуть свої особливості і в інтернеті купа відповідей на питання типу how to install docker on windows), перевірити, що все працює.
  2. Запустити MySQL сервер як докер-контейнер.
  3. Розгорнути дамп .
  4. Створити проект у Ідеї, додати залежності maven.
  5. Зробити шар domain.
  6. Написати метод отримання всіх даних із MySQL.
  7. Написати метод трансформації даних (в Redis писатимемо лише ті дані, які запитуються часто).
  8. Запустити Redis сервер як докер-контейнер.
  9. Записати дані у Redis.
  10. Опціонально: встановити redis-insight, подивитися на дані, що зберігаються у Redis.
  11. Написати метод отримання даних із Redis.
  12. Написати метод отримання даних з MySQL.
  13. Порівняти швидкість отримання тих самих даних з MySQL і Redis.

Налаштування Docker

Докер – це відкрита платформа для розробки, доставки та експлуатації програм. Ми його використовуватимемо для того, щоб не встановлювати і налаштовувати Redis на локальній машині, а використовувати вже готовий image. Докладніше про докер можна почитати тут або подивитися тут . Якщо не знайомий з докером - рекомендую подивитися саме друге посилання.

Щоб переконатися, що докер в тебе встановлений і налаштований, виконай команду:docker -v

Якщо всі ОК, ти побачиш версію докера

Запустити MySQL сервер як докер-контейнер

Для того, щоб можна було порівняти час віддачі даних з MySQL та Redis, MySQL також будемо використовувати в докері. У PowerShell (або іншому консольному терміналі, якщо у тебе не Windows) виконай команду:

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

Розглянемо, що ми робимо цією командою:

  • docker run- запуск (і скачування, якщо він ще не завантажений на локальну машину) іміджу. Внаслідок запуску отримаємо запущений контейнер.
  • --name mysql- Задаємо ім'я контейнера mysql.
  • -d– прапор, який каже, що контейнер повинен продовжувати працювати, навіть якщо закрити вікно терміналу, звідки контейнер запускався.
  • -p 3306:3306- Вказує порти. До двокрапки – порт на локальній машині, після двокрапки – порт у контейнері.
  • -e MYSQL_ROOT_PASSWORD=root– передача змінної оточення MYSQL_ROOT_PASSWORD зі значенням root у контейнер. Прапор специфічний саме для образу mysql/
  • --restart unless-stopped– встановлення політики поведінки (чи контейнер повинен перезапускатися під час закриття). Значення unless-stopped означає перезапускати завжди, крім випадку, коли контейнер був зупинений /
  • -v mysql:/var/lib/mysql – додати volume (образ для зберігання інформації).
  • mysql:8 - Назва іміджу та його версія.

Після виконання команди в терміналі докер скачає всі шари іміджу та запустить контейнер:

Важливо: якщо у тебе на локальному комп'ютері встановлений MySQL як сервіс і він запущений - потрібно в команді запуску вказати інший порт, або зупинити цей запущений сервіс.

Розгорнути дамп

Для розгортання дампа потрібно з Workbench створити новий конекшн до БД, де вказати параметри. Я використовував стандартний порт (3306), не змінював ім'я користувача (root за замовчуванням) і вказав пароль для рутового користувача (root).

У Workbench-і робимо Data Import/Restoreта вибираємо Import from Self Contained File. Як файл вкажи куди ти скачав дамп . Схему заздалегідь створювати не потрібно – її створення включено до дамп-файлу. Після успішного імпорту, у тебе буде схема world із трьома таблицями:

  1. city ​​– це таблиця міст.
  2. country – таблиця держав.
  3. country_language - таблиця, в якій вказується який відсоток населення в країні говорить певною мовою.

Так як при запуску контейнера ми використовували вольюм, після зупинки і навіть видалення контейнера mysql та повторного виконання команди запуску ( docker run --name mysql -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root --restart unless-stopped -v mysql:/var/lib/mysql mysql:8), повторно дамп розгортати не потрібно буде – він вже розгорнутий у вольюмі.

Створити проект в Ідеї, додати залежності maven

Як в Ідеї створити проект, ти вже давно знаєш – це найлегший пункт у сьогоднішньому проекті.

Додаємо в 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> 

Перші три залежності тобі вже давно знайомі.

lettuce-core– це один із доступних Java клієнтів для роботи з Redis.

jackson-databind- Залежність для використання ObjectMapper (для перетворення даних для їх зберігання в Redis (ключ-значення типу String)).

Також до папки ресурсів (src/main/resources) додай spy.properties для перегляду запитів з параметрами, які виконує Hibernate. Вміст файлу:

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 

Зробити шар domain

Створи пакет com.codegym.domain

Мені зручно при мапінг таблиць на ентіті користуватися структурою таблиці в Ідеї, тому додамо підключення БД в Ідеї.

Ентіті я пропоную створювати в такому порядку:

  • Country
  • City
  • CountryLanguage

Бажано, щоб мапінг ти виконав самостійно.

Код класу Country:

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

}

У коді є 3 цікаві моменти.

Перше – це енам Continent , який у БД зберігається як ordinal значень. У структурі таблиці country у коментарі до поля continent можна подивитися, яке числове значення відповідає якому континенту.

package com.codegym.domain;

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

Другий момент - це сет сутностейCountryLanguage . Тут зв'язок @OneToMany, якого не було у другому проекті цього модуля. За промовчанням Hibernate не витягуватиме значення цього сету при запиті ентіті країни. Але так як нам потрібно віднімати всі значення реляційної БД для кешування, тут доданий параметр FetchType.EAGER.

Третє – поле city . Зв'язок @OneToOne– наче все звичне і зрозуміле. Але, якщо подивитися на структуру foreign key у БД, то бачимо, що у країни (country) є зв'язок на столицю (city), а у міста (city) – на країну (country). В наявності циклічна залежність.

Поки з цим нічого робити не будемо, але коли дійдемо до пункту «Написати метод отримання всіх даних з MySQL», подивимося які запити виконує Hibernate, подивимося на їхню кількість, і згадаємо цей пункт.Hibernate

Код класу City:

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:

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
}

Написати метод отримання всіх даних з MySQL

У класі Main оголосимо поля:

private final SessionFactory sessionFactory;
private final RedisClient redisClient;

private final ObjectMapper mapper;

private final CityDAO cityDAO;
private final CountryDAO countryDAO;

та ініціалізуємо їх у конструкторі класу Main:

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

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

Як бачиш, не вистачає методів та класів – давай їх напишемо.

Оголоси пакет com.codegym.dao та додай до нього 2 класи:

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

Тепер можна в Main імпортувати ці 2 класи. Ще не вистачає двох методів:

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

До редис ми ще не дійшли, тому імплементація ініціалізації клієнта редис поки залишиться заглушкою:

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

Зрештою можна написати метод, у якому ми витягнемо всі міста:

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

Особливість реалізації така, що міста ми отримуємо по 500 штук. Це потрібно для того, що існують обмеження на обсяг даних, що передаються. Так, у разі ми ними потрапимо, т.к. у нас у БД всього 4079 міст. Але в продакшн додатках, коли потрібно отримати багато даних, таким прийомом користуються часто.

І реалізація методу main:

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

Тепер можна вперше запустити наш додаток у дебазі і подивитися, як він працює (або не працює – так, так часто буває).

Міста дістаються. До кожного міста дістається країна, якщо вона ще не була вичитана з БД раніше іншого міста. Давай приблизно порахуємо скільки запитів Hibernate відправить до БД:

  • 1 запит дізнатися загальну кількість міст (необхідно циклу ітерування по 500 міст, щоб знати коли зупинитися).
  • 4079/500 = 9 запитів (список міст).
  • До кожного з міст дістається країна, якщо вона не була віднімається раніше. Оскільки у БД 239 країн, це нам дасть 239 запитів.

Разом 249 запитов. І це ще ми сказали, що разом із країною одразу отримувати сет мов, інакше була б взагалі морок. Але це все одно багато, тому давай трохи підтюніть поведінку. Почнемо з роздумів: що робити, куди тікати? А якщо серйозно – чому так багато запитів. Якщо дивитися на лог запитів, бачимо, що кожна країна запитується окремо, тому перше просте рішення: давай запитаємо всі країни разом, тому що ми наперед знаємо, що в цій транзакції вони нам будуть потрібні.

У методі fetchData() відразу після початку транзакції додай рядок:

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

Вважаємо запити:

  • 1 – отримати всі країни
  • 239 – запит щодо кожної країни її столиці
  • 1 – запит кількості міст
  • 9 – запит списків міст

Разом 250. Ідея гарна, але не спрацювало. Проблема в тому, що в країні зв'язок із столицею (city) @OneToOne. Такий зв'язок за замовчуванням завантажується відразу ( FetchType.EAGER). Давай поставимо FetchType.LAZY, т.к. ми все одно потім у цій же транзакції завантажимо всі міста.

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

Столиці окремо перестали просити, але кількість запитів не змінилася. Тепер по кожній країні запитується окремим запитом список CountryLanguage . Тобто прогрес є, і ми рухаємось у правильному напрямку. Якщо пам'ятаєш, у лекціях пропонувалося рішення «join fetch» ​​для того, щоб шляхом додавання додаткового джоїну запит запитувати сутність із залежними даними одним запитом. У CountryDAO перепиши HQL запит у методі getAll()на:

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

Запуск. Дивимося балку, вважаємо запити:

  • 1 – всі країни з мовами
  • 1 – кількість міст
  • 9 – списки міст.

Разом 11- у нас вийшло)) Якщо ти не просто прочитав весь цей текст, а й пробував запускати після кожного кроку тюнінгу програми, ти повинен був навіть візуально відзначити прискорення відпрацювання всього додатка в кілька разів.

Написати метод трансформації даних

Створимо пакет com.codegym.redis, в який додамо 2 класи: CityCountry (дані по місту та країні, в якій це місто розташоване) і Language (дані з мови). Сюди винесено всі поля, які «за завданням» запитуються часто в «запиті, що гальмує».

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
}

У методі main після отримання всіх міст додай рядок

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

І реалізуй цей метод:

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

Думаю, цей метод пояснення не вимагає: просто створюємо сутність CityCountry і заповнюємо даними з City , Country , CountryLanguage .

Запустити Redis сервер як докер-контейнер

Тут є два варіанти. Якщо ти робитимеш опціональний крок «встановити redis-insight, подивитися на дані, які зберігаються в Redis», тоді команда для тебе:

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

Якщо ж вирішив пропустити цей крок, тоді достатньо:

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

Різниця в тому, що в першому варіанті прокидається порт 8001 на локальну машину, до якого можна підключитися зовнішнім клієнтом, щоб подивитися, що зберігається всередині. І імена я звик давати значущі, тому redis-stackчи redis.

Після запуску можна переглянути список запущених контейнерів. Для цього виконай команду:

docker container ls 

І побачиш щось приблизно таке:

Якщо потрібно знайти якусь команду, можна або в терміналі подивитися хелп (docker help) або загуглити how to … (наприклад, docker how to remove running container).

І ще ми в конструкторі Main викликали ініціалізацію редис-клієнта, але сам метод не реалізували. Додай реалізацію:

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 доданий у навчальних цілях щоб у лозі запуску бачити, що всі ОК і конекшн через редис-клієнт пройшов без помилок.

Записати дані в Redis

У метод main додай виклик

main.pushToRedis(preparedData); 

З такою реалізацією методу:

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

    }
}

Тут відкривається синхронне з'єднання з редис-клієнтом, і послідовно кожен об'єкт типу CityCountry пишеться в редис. Так як редиска - це сховище типу ключ-значення типу String , ключ (id міста) перетворюється на рядок. А значення теж до рядка, але використовуючи ObjectMapper в JSON формат.

Залишилося запустити і перевірити, чи немає помилок у лозі. Усе відпрацювало.

Встановити redis-insight, переглянути дані, які зберігаються в Redis (опціонально)

Завантажуємо за посиланням redis-insight та встановлюємо його. Після запуску в мене одразу показує наш інстанс редиски в докер-контейнері:

Якщо зайти – побачимо список усіх ключів:

І можна зайти на будь-який ключ, щоб подивитися які дані щодо нього зберігаються:

Написати метод отримання даних з Redis

Для тестування використовуємо такий тест: отримаємо 10 записів CityCountry. Кожен окремим запитом, але в одному конекшні.

Дані з редис можна отримати через наш редис-клієнт. Для цього напишемо метод, який приймає список ID, які потрібно отримати.

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

Реалізація, думаю, інтуїтивно-зрозуміла: відкриваємо синхронне з'єднання, і для кожного id отримуємо JSON String , який перетворюємо на потрібний нам об'єкт типу CityCountry .

Написати метод отримання даних з MySQL

У класі CityDAO додай метод getById(Integer id), в якому отримаємо місто разом із країною:

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

За аналогією з попереднім пунктом, додамо до класу Main аналогічний метод для MySQL:

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

З особливостей, щоб, напевно, отримати повний об'єкт (без проксі-заглушок), явно запитимо в країни список мов.

Порівняти швидкість отримання одних і тих самих даних з MySQL та Redis

Тут наведу одразу код методу main, і результат, який виходить на моєму локальному комп'ютері.

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

    //закриємо поточну сесію, щоб точно робити запит до БД, а чи не витягнути дані з кешу
    main.sessionFactory.getCurrentSession().close();

    //вибираємо випадкових 10 id міст
    //оскільки ми не робабо обробку невалідних ситуацій, використовуй існуючі в БД id
    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();
}

При тестуванні є особливість – дані з MySQL лише читаються, тому його можна не перезапускати між запусками нашої програми. А в Redis пишуться.

Хоча при спробі додати дублікат за таким же ключем, дані просто оновляться, я б рекомендував між запусками програми в терміналі виконувати команди зупинки контейнера docker stop redis-stackта видалення контейнера docker rm redis-stack. Після цього заново піднімати контейнер із редисом docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:latestі лише після цього виконувати нашу програму.

Ось мої результати тестування:

Отже, ми домоглися підвищення продуктивності відповіді на «частий гальмуючий» запит у півтора рази. І це ще з урахуванням того, що в тестуванні ми використовували не найшвидшу десеріалізацію через ObjectMapper . Якщо її поміняти на GSON, швидше за все, можна "виграти" ще трохи часу.

У цей момент мені згадується прикол про програміста і час: почитайте і подумайте, як потрібно писати та оптимізувати свій код.