Hoje faremos o projeto final do quarto módulo JRU. O que será? Vamos tentar trabalhar com diferentes tecnologias: MySQL, Hibernate, Redis, Docker. Agora mais assunto.

Tarefa: temos um banco de dados MySQL relacional com um esquema (país-cidade, idioma por país). E há um pedido frequente da cidade, que desacelera. Encontramos uma solução - mover todos os dados solicitados com frequência para o Redis (no armazenamento de memória do tipo valor-chave).

E não precisamos de todos os dados armazenados no MySQL, mas apenas de um conjunto selecionado de campos. O projeto será em forma de tutorial. Ou seja, aqui vamos levantar o problema e resolvê-lo imediatamente.

Então, vamos começar com o software que precisaremos:

  1. IDEA Ultimate (quem ficou sem a chave - escreva para Roman no Slack)
  2. Workbench (ou qualquer outro cliente para MySQL)
  3. Docker
  4. redis-insight - opcional

Nosso plano de ação:

  1. Configure o docker (não farei isso no tutorial, pois cada sistema operacional terá suas próprias características e há muitas respostas na Internet para perguntas como "como instalar o docker no windows"), verifique se está tudo funcionando.
  2. Execute o servidor MySQL como um contêiner docker.
  3. Expandir despejo .
  4. Crie um projeto no Idea, adicione dependências maven.
  5. Faça o domínio da camada.
  6. Escreva um método para obter todos os dados do MySQL.
  7. Escreva um método de transformação de dados (no Redis, escreveremos apenas os dados solicitados com frequência).
  8. Execute o servidor Redis como um contêiner docker.
  9. Gravar dados no Redis.
  10. Opcional: instale o redis-insight, veja os dados armazenados no Redis.
  11. Escreva um método para obter dados do Redis.
  12. Escreva um método para obter dados do MySQL.
  13. Compare a velocidade de obter os mesmos dados do MySQL e do Redis.

Configuração do Docker

Docker é uma plataforma aberta para desenvolvimento, entrega e operação de aplicativos. Vamos usá-lo para não instalar e configurar o Redis na máquina local, mas para usar uma imagem pronta. Você pode ler mais sobre o docker aqui ou vê-lo aqui . Se você não estiver familiarizado com o docker, recomendo olhar apenas o segundo link.

Para garantir que você tenha o docker instalado e configurado, execute o comando:docker -v

Se tudo estiver OK, você verá a versão do docker

Execute o servidor MySQL como contêiner docker

Para poder comparar o tempo de retorno dos dados do MySQL e do Redis, também usaremos o MySQL no docker. No PowerShell (ou outro terminal de console se você não estiver usando o Windows), execute o comando:

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

Considere o que estamos fazendo com este comando:

  • docker run– iniciar (e baixar, caso ainda não tenha sido baixado na máquina local) a imagem. Como resultado do lançamento, obtemos um contêiner em execução.
  • --name mysql- defina o nome do contêiner mysql.
  • -d- um sinalizador que diz que o contêiner deve continuar funcionando, mesmo que você feche a janela do terminal de onde esse contêiner foi lançado.
  • -p 3306:3306- especifica as portas. Antes dos dois pontos - a porta na máquina local, depois dos dois pontos - a porta no contêiner.
  • -e MYSQL_ROOT_PASSWORD=root– passando a variável de ambiente MYSQL_ROOT_PASSWORD com o valor root para o container. Sinalizador específico para a imagem mysql/
  • --restart unless-stopped- definir a política de comportamento (se o contêiner deve ser reiniciado quando fechado). O valor except-stopped significa sempre reiniciar, exceto quando o container foi parado/
  • -v mysql:/var/lib/mysql – adicionar volume (imagem para armazenar informações).
  • mysql:8 – o nome da imagem e sua versão.

Após executar o comando no terminal, o docker irá baixar todas as camadas da imagem e iniciar o container:

Observação importante: se você tiver o MySQL instalado como um serviço em seu computador local e ele estiver em execução, você precisará especificar uma porta diferente no comando start ou interromper este serviço em execução.

Expandir despejo

Para expandir o dump, você precisa criar uma nova conexão com o banco de dados do Workbench, onde você especifica os parâmetros. Usei a porta padrão (3306), não alterei o nome de usuário (root por padrão) e defini a senha para o usuário root (root).

No Workbench, faça Data Import/Restoree selecione Import from Self Contained File. Especifique onde você baixou o dump como um arquivo . Você não precisa criar o esquema de antemão - sua criação está incluída no arquivo dump. Após uma importação bem-sucedida, você terá um esquema de mundo com três tabelas:

  1. city ​​é uma tabela de cidades.
  2. país - tabela de países.
  3. country_language - uma tabela que indica qual porcentagem da população do país fala um determinado idioma.

Como utilizamos um volume ao iniciar o container, após parar e até deletar o container mysql e reexecutar o comando start ( docker run --name mysql -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root --restart unless-stopped -v mysql:/var/lib/mysql mysql:8), não haverá necessidade de implantar o dump novamente - ele já está implantado no volume.

Criar projeto no Idea, adicionar dependências maven

Você já sabe como criar um projeto no Idea - este é o ponto mais fácil do projeto de hoje.

Adicione dependências ao arquivo 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> 

As três primeiras dependências são familiares para você há muito tempo.

lettuce-coreé um dos clientes Java disponíveis para trabalhar com Redis.

jackson-databind– dependência para uso do ObjectMapper (para transformar dados para armazenamento em Redis (chave-valor do tipo String)).

Também na pasta de recursos (src/main/resources) adicione spy.properties para visualizar as requisições com parâmetros que o Hibernate executa. Conteúdo do arquivo:

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 

Tornar o domínio da camada

Criar pacote com.codegym.domain

É conveniente para mim ao mapear tabelas em uma entidade usar a estrutura da tabela no Idea, então vamos adicionar uma conexão de banco de dados no Idea.

Sugiro criar entidades nesta ordem:

  • País
  • cidade
  • Idioma do país

É desejável que você mesmo execute o mapeamento.

Código da classe do país:

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

}

Existem 3 pontos interessantes no código.

O primeiro é o Continent enam , que é armazenado no banco de dados como valores ordinais. Na estrutura da tabela de países, nos comentários ao campo continente, você pode ver qual valor numérico corresponde a qual continente.

package com.codegym.domain;

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

O segundo ponto é um conjunto de entidadesCountryLanguage . Aqui está um link @OneToManyque não estava no segundo rascunho deste módulo. Por padrão, o Hibernate não puxará o valor deste conjunto ao solicitar uma entidade de país. Mas como precisamos subtrair todos os valores do banco de dados relacional para cache, o arquivo FetchType.EAGER.

O terceiro é o campo da cidade . Comunicação @OneToOne- como tudo é familiar e compreensível. Mas, se observarmos a estrutura da chave estrangeira no banco de dados, veremos que o país (país) tem um link para a capital (city) e a cidade (city) tem um link para o país (country). Existe uma relação cíclica.

Ainda não faremos nada com isso, mas quando chegarmos ao item “Escreva um método para obter todos os dados do MySQL”, veremos quais consultas o Hibernate executa, observe seu número e lembre-se deste item. Hibernate

Código da classe da cidade:

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

}

Código da classe 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
}

Escreva um método para obter todos os dados do MySQL

Na classe Main, declaramos os campos:

private final SessionFactory sessionFactory;
private final RedisClient redisClient;

private final ObjectMapper mapper;

private final CityDAO cityDAO;
private final CountryDAO countryDAO;

e inicialize-os no construtor da classe Main:

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

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

Como você pode ver, não há métodos e classes suficientes - vamos escrevê-los.

Declare um pacote com.codegym.dao e adicione 2 classes a ele:

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

Agora você pode importar essas 2 classes para Main. Ainda faltam dois métodos:

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

Ainda não alcançamos o rabanete, então a implementação da inicialização do cliente rabanete permanecerá um esboço por enquanto:

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

Por fim, podemos escrever um método no qual extraímos todas as cidades:

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

O recurso de implementação é tal que obtemos 500 cidades cada. Isso é necessário porque há restrições na quantidade de dados transmitidos. Sim, no nosso caso, não vamos chegar até eles, porque. temos um total de 4079 cidades no banco de dados. Mas em aplicativos de produção, quando você precisa obter muitos dados, essa técnica é frequentemente usada.

E a implementação do método main:

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

Agora podemos executar nosso aplicativo em depuração pela primeira vez e ver como ele funciona (ou não funciona - sim, acontece com frequência).

As cidades estão ficando. Cada cidade recebe um país, caso não tenha sido previamente subtraído do banco de dados para outra cidade. Vamos calcular aproximadamente quantas consultas o Hibernate enviará ao banco de dados:

  • 1 solicitação para descobrir o número total de cidades (necessário iterar mais de 500 cidades para saber quando parar).
  • 4079 / 500 = 9 solicitações (lista de cidades).
  • Cada cidade recebe um país, se não tiver sido subtraído anteriormente. Como há 239 países no banco de dados, isso nos dará 239 consultas.

Total 249 requests. E também dissemos que receberíamos imediatamente um conjunto de idiomas junto com o país, caso contrário, haveria escuridão em geral. Mas ainda é muito, então vamos ajustar um pouco o comportamento. Vamos começar com reflexões: o que fazer, para onde correr? Mas falando sério - por que há tantos pedidos. Se você olhar o log de pedidos, vemos que cada país é solicitado separadamente, então a primeira solução simples: vamos solicitar todos os países juntos, pois sabemos de antemão que vamos precisar de todos eles nessa transação.

No método fetchData(), imediatamente após o início da transação, adicione a seguinte linha:

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

Contamos pedidos:

  • 1 - obter todos os países
  • 239 - consulta para cada país de sua capital
  • 1 - solicitação do número de cidades
  • 9 - solicitação de listas de cidades

Total 250. A ideia é boa, mas não funcionou. O problema é que o país tem ligação com a capital (cidade) @OneToOne. E esse link é carregado imediatamente por padrão ( FetchType.EAGER). Vamos colocar FetchType.LAZY, porque de qualquer forma, carregaremos todas as cidades posteriormente na mesma transação.

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

As maiúsculas não são mais solicitadas separadamente, mas o número de solicitações não mudou. Agora, para cada país, a lista CountryLanguage é solicitada por uma consulta separada . Ou seja, há progresso e estamos caminhando na direção certa. Se você se lembra, as palestras sugeriram a solução “join fetch” para solicitar uma entidade com dados dependentes em uma solicitação adicionando uma junção adicional à solicitação. Em CountryDAO , reescreva a consulta HQL no método getAll()para:

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

Lançar. Nós olhamos para o log, contamos as solicitações:

  • 1 - todos os países com idiomas
  • 1 - número de cidades
  • 9 - listas de cidades.

Total 11- conseguimos)) Se você não apenas leu todo este texto, mas também tentou executá-lo após cada etapa de ajuste do aplicativo, você deve observar visualmente a aceleração de todo o aplicativo várias vezes.

Escreva um método de transformação de dados

Vamos criar um pacote com.codegym.redisno qual adicionamos 2 classes: CityCountry (dados sobre a cidade e o país em que esta cidade está localizada) e Language (dados sobre o idioma). Aqui estão todos os campos frequentemente solicitados “por tarefa” no “pedido de frenagem”.

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
}

No método main, após pegar todas as cidades, adicione a linha

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

E implemente este método:

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

Acho que esse método é autoexplicativo: apenas criamos uma entidade CityCountry e a preenchemos com dados de City , Country , CountryLanguage .

Execute o servidor Redis como um contêiner docker

Existem 2 opções aqui. Se você executar a etapa opcional "instalar o redis-insight, observar os dados armazenados no Redis", o comando é para você:

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

Se você decidir pular esta etapa, basta:

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

A diferença é que na primeira opção, a porta 8001 é encaminhada para a máquina local, onde você pode se conectar com um cliente externo para ver o que está armazenado lá dentro. E eu costumava dar nomes significativos, portanto, redis-stackou redis.

Após o lançamento, você pode ver a lista de contêineres em execução. Para fazer isso, execute o comando:

docker container ls 

E você verá algo assim:

Se você precisar encontrar algum comando, pode consultar a ajuda no terminal (ajuda do docker) ou pesquisar no Google "como ..." (por exemplo, docker como remover o contêiner em execução).

E também chamamos a inicialização do cliente rabanete no construtor Main, mas não implementamos o método em si. Adicionar implementação:

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 foi adicionado para fins educacionais para que no log de inicialização você possa ver que está tudo bem e a conexão pelo cliente radish passou sem erros.

Gravar dados no Redis

Adicionar uma chamada ao método principal

main.pushToRedis(preparedData); 

Com esta implementação do método:

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

    }
}

Aqui, uma conexão síncrona é aberta com o cliente rabanete e sequencialmente cada objeto do tipo CityCountry é gravado no rabanete. Como o rabanete é um armazenamento de valor-chave String , a chave (id da cidade) é convertida em uma string. E o valor também é para a string, mas usando o ObjectMapper no formato JSON.

Resta executar e verificar se não há erros no log. Tudo funcionou.

Instale o redis-insight, veja os dados armazenados no Redis (opcional)

Baixe o redis-insight no link e instale-o. Depois de iniciar, ele mostra imediatamente nossa instância de rabanete no contêiner docker:

Se você fizer login, veremos uma lista de todas as chaves:

E você pode acessar qualquer tecla para ver quais dados estão armazenados nela:

Escreva um método para obter dados do Redis

Para testar, usamos o seguinte teste: obtemos 10 registros CityCountry. Cada um com um pedido separado, mas em uma conexão.

Os dados do rabanete podem ser obtidos através do nosso cliente rabanete. Para fazer isso, vamos escrever um método que receba uma lista de id's.

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

A implementação, penso eu, é intuitiva: abrimos uma conexão síncrona e para cada id obtemos um JSON String , que convertemos no objeto do tipo CityCountry que precisamos .

Escreva um método para obter dados do MySQL

Na classe CityDAO , adicione um método getById(Integer id)no qual obteremos a cidade junto com o país:

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

Por analogia com o parágrafo anterior, vamos adicionar um método semelhante para MySQL à classe 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();
    }
}

Dos recursos, para ter certeza de obter o objeto completo (sem stubs de proxy), solicitamos explicitamente uma lista de idiomas do país.

Compare a velocidade de obtenção dos mesmos dados do MySQL e do Redis

Aqui darei imediatamente o código do método principal e o resultado obtido no meu computador local.

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

Ao testar, há um recurso - os dados do MySQL são apenas lidos, portanto, não podem ser reiniciados entre as inicializações de nosso aplicativo. E no Redis eles são escritos.

Embora quando você tentar adicionar uma duplicata para a mesma chave, os dados simplesmente serão atualizados, recomendo que você execute os comandos para parar o contêiner docker stop redis-stacke excluí-lo entre as inicializações do aplicativo no terminal docker rm redis-stack. Depois disso, levante o recipiente com o rabanete novamente docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:lateste só depois disso execute nosso aplicativo.

Aqui estão os resultados dos meus testes:

No total, conseguimos um aumento no desempenho da resposta à solicitação de "frenagem frequente" em uma vez e meia. E isso levando em consideração o fato de que nos testes não usamos a desserialização mais rápida por meio do ObjectMapper . Se você mudar para GSON, provavelmente poderá "ganhar" um pouco mais de tempo.

Neste momento, lembro-me de uma piada sobre um programador e o tempo: leia e pense em como escrever e otimizar seu código.