Днес ще направим финалния проект на четвъртия JRU модул. Какво ще бъде? Нека се опитаме да работим с различни технологии: MySQL, Hibernate, Redis, Docker. Сега повече тема.

Задача: имаме релационна MySQL база данни със схема (държава-град, език по държава). И има често искане на града, което забавя. Измислихме решение - да преместим всички данни, които се изискват често, в Redis (в памет за съхранение от типа ключ-стойност).

И ние не се нуждаем от всички данни, които се съхраняват в MySQL, а само от избран набор от полета. Проектът ще бъде под формата на самоучител. Тоест тук ще повдигнем проблема и веднага ще го решим.

И така, нека започнем с това Howъв софтуер ще ни е необходим:

  1. IDEA Ultimate (който е изчерпал ключа - пишете на Roman в Slack)
  2. Workbench (or всеки друг клиент за MySQL)
  3. Докер
  4. redis-insight - по избор

Нашият план за действие:

  1. Настройте докер (няма да правя това в урока, защото всяка операционна система ще има свои собствени характеристики и в интернет има много отговори на въпроси като „How да инсталирате докер на Windows“), проверете дали всичко работи.
  2. Стартирайте MySQL сървър като докер контейнер.
  3. Разширяване на дъмп .
  4. Създайте проект в Idea, добавете зависимости на maven.
  5. Направете домейн на слоя.
  6. Напишете метод за получаване на всички данни от MySQL.
  7. Напишете метод за трансформиране на данни (в Redis ще записваме само данните, които се изискват често).
  8. Стартирайте Redis сървъра като докер контейнер.
  9. Запишете данни в Redis.
  10. По избор: инсталирайте redis-insight, прегледайте данните, съхранени в Redis.
  11. Напишете метод за получаване на данни от Redis.
  12. Напишете метод за получаване на данни от MySQL.
  13. Сравнете скоростта на получаване на едни и същи данни от MySQL и Redis.

Настройка на Docker

Docker е отворена платформа за разработване, доставяне и работа с applications. Ще го използваме, за да не инсталираме и конфигурираме Redis на локалната машина, а да използваме готово изображение. Можете да прочетете повече за docker тук or да го видите тук . Ако не сте запознати с docker, препоръчвам ви да погледнете само втората връзка.

За да сте сигурни, че имате инсталиран и конфигуриран докер, изпълнете командата:docker -v

Ако всичко е наред, ще видите докер versionта

Стартирайте MySQL сървър като докер контейнер

За да можем да сравним времето за връщане на данни от MySQL и Redis, ние също ще използваме MySQL в докера. В PowerShell (or друг конзолен терминал, ако не използвате Windows), изпълнете командата:

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

Помислете Howво правим с тази команда:

  • 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 – добавяне на обем (изображение за съхранение на информация).
  • mysql:8 – името на изображението и неговата version.

След като изпълни командата в терминала, докерът ще изтегли всички слоеве на изображението и ще стартира контейнера:

Важна забележка: ако имате инсталиран MySQL като услуга на вашия локален компютър и тя работи, трябва да посочите различен порт в командата за стартиране or да спрете тази работеща услуга.

Разширяване на дъмп

За да разширите дъмпа, трябва да създадете нова връзка към базата данни от Workbench, където задавате параметрите. Използвах порта по подразбиране (3306), не промених потребителското име (root по подразбиране) и зададох паролата за root потребител (root).

В Workbench направете Data Import/Restoreи изберете Import from Self Contained File. Посочете къде сте изтеглor дъмпа като файл . Не е необходимо да създавате схемата предварително - нейното създаване е включено в дъмп file. След успешно импортиране ще имате световна схема с три таблици:

  1. city ​​​​е table на градовете.
  2. държава - table на страната.
  3. country_language - table, която показва Howъв процент от населението в страната говори определен език.

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

Създайте проект в Idea, добавете зависимости от maven

Вече знаете How да създадете проект в Idea - това е най-лесният момент в днешния проект.

Добавяне на зависимости към pom file:


<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 изпълнява. Съдържание на file:

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 

Направете домейн на слоя

Създайте пакет com.codegym.domain

За мен е удобно, когато картографирам таблици върху обект, да използвам структурата на tableта в Idea, така че нека добавим връзка с база данни в Idea.

Предлагам да създадете обекти в този ред:

  • Държава
  • град
  • CountryLanguage

Желателно е сами да извършите картографирането.

Код на класа на държавата:

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

}

В codeа има 3 интересни точки.

Първият е Continent enam , който се съхранява в базата данни като поредни стойности. В структурата на tableта на държавите, в коментарите към полето за континент, можете да видите коя цифрова стойност на кой континент отговаря.

package com.codegym.domain;

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

Втората точка е набор от обектиCountryLanguage . Ето връзка @OneToMany, която не беше във втората чернова на този модул. По подразбиране Hibernate няма да изтегли стойността на този набор, когато изисква обект на държава. Но тъй като трябва да извадим всички стойности от релационната база данни за кеширане, FetchType.EAGER.

Третото е градското поле . Общуването @OneToOne- като всичко е познато и разбираемо. Но ако погледнем структурата на външния ключ в базата данни, виждаме, че държавата (държавата) има връзка към столицата (град), а градът (град) има връзка към държавата (държава). Има циклична връзка.

Все още няма да правим нищо с това, но когато стигнем до елемента „Напишете метод за получаване на всички данни от MySQL“, нека да видим Howви заявки изпълнява Hibernate, да погледнем броя им и да запомним този елемент.

Код на класа на града:

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

В основния клас ние декларираме полетата:

private final SessionFactory sessionFactory;
private final RedisClient redisClient;

private final ObjectMapper mapper;

private final CityDAO cityDAO;
private final CountryDAO countryDAO;

и ги инициализирайте в конструктора на главния клас:

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

Сега можете да импортирате тези 2 класа в Main. Все още липсват два метода:

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 града в базата данни. Но в производствените applications, когато трябва да получите много данни, тази техника често се използва.

И изпълнението на основния метод:

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

Сега можем да стартираме нашето приложение в debug за първи път и да видим How работи (or не работи - да, често се случва).

Градовете стават. Всеки град получава държава, ако преди това не е била извадена от базата данни за друг град. Нека грубо изчислим колко заявки ще изпрати Hibernate към базата данни:

  • 1 заявка за откриване на общия брой градове (необходими за повторение на над 500 града, за да знаете кога да спрете).
  • 4079 / 500 = 9 заявки (списък с градове).
  • Всеки град получава държава, ако не е била извадена по-рано. Тъй като в базата данни има 239 държави, това ще ни даде 239 заявки.

Total 249 requests. И ние също казахме, че заедно със страната веднага ще получим набор от езици, иначе ще настъпи мрак като цяло. Но все още е много, така че нека променим малко поведението. Да започнем с размисли: Howво да правя, къде да бягам? Но сериозно - защо има толкова много заявки. Ако погледнете дневника на заявките, виждаме, че всяка държава се иска отделно, така че първото просто решение: нека поискаме всички държави заедно, защото знаем предварително, че ще имаме нужда от всички в тази транзакция.

В метода fetchData(), веднага след началото на транзакцията, добавете следния ред:

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

Ние броим заявките:

  • 1 - вземете всички държави
  • 239 - запитване за всяка страна от нейната столица
  • 1 - запитване за броя на градовете
  • 9 - искане за списъци с градове

Total 250. Идеята е добра, но не се получи. Проблемът е, че страната има връзка със столицата (град) @OneToOne. И такава връзка се зарежда веднага по подразбиране ( FetchType.EAGER). Да сложим FetchType.LAZY, защото Howто и да е, ще заредим всички градове по-късно в същата транзакция.

@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 - списъци с градове.

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

В основния метод, след получаване на всички градове, добавете линията

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 сървър като докер контейнер

Тук има 2 варианта. Ако направите незадължителната стъпка „инсталирайте 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 се препраща към локалната машина, към която можете да се свържете с външен клиент, за да видите Howво се съхранява вътре. И аз давам смислени имена, следователно, redis-stackor redis.

След стартиране можете да видите списъка с работещи контейнери. За да направите това, изпълнете командата:

docker container ls 

И ще видите нещо подобно:

Ако трябва да намерите няHowва команда, можете or да погледнете помощта в терминала (докер помощ) or да потърсите в Google „How да ...“ (например докер How да премахнете работещ контейнер).

И също така извикахме инициализацията на клиента на репички в главния конструктор, но не внедрихме самия метод. Добавяне на изпълнение:

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.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 се записва в ряпата. Тъй като ряпата е хранorще за ключ-стойност String , ключът (идентификатор на град) се преобразува в низ. И стойността също е към низа, но използвайки ObjectMapper във формат JSON.

Остава да стартирате и да проверите дали няма грешки в дневника. Всичко работеше.

Инсталирайте redis-insight, прегледайте данните, съхранени в Redis (по избор)

Изтеглете redis-insight от връзката и го инсталирайте. След като стартира, той незабавно показва нашия екземпляр на репички в докер контейнера:

Ако влезете, ще видим списък с всички ключове:

И можете да отидете на всеки ключ, за да видите Howви данни се съхраняват в него:

Напишете метод за получаване на данни от Redis

За тестване използваме следния тест: получаваме 10 CityCountry записа. Всяка с отделна заявка, но в една връзка.

Данни от репички могат да бъдат получени чрез нашия клиент за репички. За да направим това, нека напишем метод, който взема списък с идентификатори за получаване.

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

Изпълнението, според мен, е интуитивно: отваряме синхронна връзка и за всеки идентификатор получаваме 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();
}

По аналогия с предишния параграф, нека добавим подобен метод за 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

Тук веднага ще дам codeа на основния метод и резултата, който се получава на моя локален компютър.

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

При тестване има функция - данните от 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, най-вероятно можете да "спечелите" малко повече време.

В този момент се сещам за един виц за програмист и време: прочетете и помислете How да напишете и оптимизирате codeа си.