今日は、4 番目の JRU モジュールの最後のプロジェクトを実行します。それは何でしょうか?MySQL、Hibernate、Redis、Docker などのさまざまなテクノロジーを使ってみましょう。さて、さらに主題です。

タスク: スキーマ (国-都市、国ごとの言語) を備えたリレーショナル MySQL データベースがあります。そして市からの要望も頻繁にあり、スピードが落ちてしまいます。私たちは、頻繁にリクエストされるすべてのデータを Redis (キー/値型のメモリ ストレージ内) に移動するという解決策を考え出しました。

また、MySQL に保存されているすべてのデータが必要なわけではなく、選択されたフィールドのセットのみが必要です。プロジェクトはチュートリアルの形式になります。つまり、ここで問題を提起し、すぐに解決します。

それでは、必要なソフトウェアから始めましょう。

  1. IDEA Ultimate (キーがなくなった人 - Slack で Roman に手紙を書きます)
  2. ワークベンチ (または MySQL のその他のクライアント)
  3. ドッカー
  4. redis-insight - オプション

私たちの行動計画:

  1. docker をセットアップし (チュートリアルではこれを行いません。OS ごとに独自の特性があり、「Windows に docker をインストールする方法」などの質問に対する答えがインターネット上にたくさんあるためです)、すべてが動作することを確認します。
  2. MySQL サーバーを Docker コンテナとして実行します。
  3. ダンプを展開します。
  4. Idea でプロジェクトを作成し、Maven の依存関係を追加します。
  5. レイヤードメインを作成します。
  6. MySQL からすべてのデータを取得するメソッドを作成します。
  7. データ変換メソッドを作成します (Redis では、頻繁に要求されるデータのみを書き込みます)。
  8. Redis サーバーを Docker コンテナーとして実行します。
  9. データを Redis に書き込みます。
  10. オプション: redis-insight をインストールし、Redis に保存されているデータを確認します。
  11. Redis からデータを取得するメソッドを作成します。
  12. MySQL からデータを取得するメソッドを作成します。
  13. MySQL と Redis から同じデータを取得する速度を比較します。

Docker のセットアップ

Docker は、アプリケーションを開発、配信、運用するためのオープン プラットフォームです。Redis をローカル マシンにインストールして構成するのではなく、既製のイメージを使用するためにこれを使用します。docker の詳細については、ここで読むか、ここで参照できます。docker に詳しくない場合は、2 番目のリンクだけを参照することをお勧めします。

docker がインストールされ、構成されていることを確認するには、次のコマンドを実行します。docker -v

すべて問題なければ、Docker のバージョンが表示されます。

MySQL サーバーを Docker コンテナとして実行する

MySQL と Redis からデータが返される時間を比較できるようにするために、Docker でも 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/image に固有のフラグ
  • --restart unless-stopped- 動作ポリシーの設定 (コンテナを閉じたときにコンテナを再起動するかどうか)。停止しない場合の値は、コンテナーが停止した場合を除き、常に再起動することを意味します。
  • -v mysql:/var/lib/mysql – ボリューム(情報を保存するためのイメージ)を追加します。
  • mysql:8 – イメージの名前とそのバージョン。

ターミナルでコマンドを実行すると、Docker はイメージのすべてのレイヤーをダウンロードし、コンテナーを開始します。

重要な注意事項: MySQL をローカル コンピューターにサービスとしてインストールし、実行している場合は、開始コマンドで別のポートを指定するか、この実行中のサービスを停止する必要があります。

ダンプを展開する

ダンプを展開するには、Workbench からデータベースへの新しい接続を作成し、パラメータを指定する必要があります。デフォルトのポート (3306) を使用し、ユーザー名 (デフォルトでは root) を変更せず、root ユーザー (root) のパスワードを設定しました。

ワークベンチで、 を実行してData Import/Restore選択しますImport from Self Contained Fileダンプをファイルとしてダウンロードした場所を指定します。事前にスキーマを作成する必要はありません。スキーマの作成はダンプ ファイルに含まれています。インポートが成功すると、3 つのテーブルを含むワールド スキーマが作成されます。

  1. city は都市のテーブルです。
  2. 国 - 国のテーブル。
  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ダンプを再度デプロイする必要はありません。ダンプはすでにボリュームにデプロイされています。

Idea でプロジェクトを作成し、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> 

最初の 3 つの依存関係は、長い間よく知られています。

lettuce-coreRedis を操作するために使用できる Java クライアントの 1 つです。

jackson-databind– ObjectMapper を使用するための依存関係 (Redis に保存するためにデータを変換するため) (文字列型のキーと値)。

また、リソース フォルダー (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 

レイヤードメインの作成

パッケージcom.codegym.domainを作成する

エンティティのテーブルをマッピングするときにアイデアのテーブル構造を使用すると便利なので、アイデアにデータベース接続を追加しましょう。

次の順序でエンティティを作成することをお勧めします。

  • 国言語

マッピングは自分で実行することが望ましいです。

国別クラスコード:

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 つあります。

1 つ目は Continent enam で、データベースに序数値として保存されます。国テーブルの構造では、大陸フィールドへのコメントで、どの数値がどの大陸に対応しているかがわかります。

package com.codegym.domain;

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

2 番目のポイントはエンティティのセットですCountryLanguage@OneToManyこのモジュールの第 2 ドラフトには含まれていなかったリンクを次に示します。デフォルトでは、Hibernate は国エンティティをリクエストするときにこのセットの値を取得しません。ただし、キャッシュのためにリレーショナル データベースからすべての値を差し引く必要があるため、FetchType.EAGER.

3つ目は都市フィールドです。コミュニケーション@OneToOne- すべてが親しみやすく、理解できるものであるかのように。しかし、データベース内の外部キー構造を見ると、国 (国) には首都 (都市) へのリンクがあり、都市 (都市) には国 (国) へのリンクがあることがわかります。循環関係があります。

これについてはまだ何も行いませんが、「MySQL からすべてのデータを取得するメソッドを作成する」項目に到達したら、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 からすべてのデータを取得するメソッドを作成する

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

これで、これら 2 つのクラスを 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 個のクエリが返されます。

Total 249 requestов。そして私たちはまた、国とともに一連の言語をすぐに受け取るだろう、そうしなければ全体的に暗闇が訪れるだろうとも言いました。しかし、まだ量が多いので、動作を少し調整しましょう。まずは反省から始めましょう。何をすべきか、どこに逃げるべきか。しかし真剣に考えてみると、なぜこれほど多くのリクエストがあるのでしょうか。リクエスト ログを見ると、各国が個別にリクエストされていることがわかります。そのため、最初の簡単な解決策は、このトランザクションですべての国が必要になることが事前にわかっているため、すべての国を一緒にリクエストしましょう。

fetchData() メソッドで、トランザクションの開始直後に次の行を追加します。

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

リクエストをカウントします:

  • 1 - すべての国を取得します
  • 239 - 各国の首都のクエリ
  • 1 - 都市数のリクエスト
  • 9 - 都市のリストのリクエスト

Total 250。アイデアは良いのですが、うまくいきませんでした。問題は、その国が首都(都市)と結びついていることだ@OneToOne。そして、そのようなリンクはデフォルトですぐにロードされます ( FetchType.EAGER)。を入れましょうFetchType.LAZY。とにかく、後で同じトランザクションですべての都市をロードします。

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

資本は個別に要求されなくなりましたが、要求の数は変わりません。ここで、国ごとに、個別のクエリによってCountryLanguageリストが要求されます。つまり、進歩があり、正しい方向に進んでいます。覚えていると思いますが、講義では、リクエストに追加の結合を追加することで、1 つのリクエストで依存データを持つエンティティをリクエストするための「結合フェッチ」ソリューションを提案しました。CountryDAOで、メソッド内の HQL クエリを次getAll()のように書き換えます。

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

発売。ログを確認し、リクエストをカウントします。

  • 1 - 言語のあるすべての国
  • 1 - 都市の数
  • 9 - 都市のリスト。

Total 11- 成功しました)) このテキストをすべて読んだだけでなく、アプリケーションのチューニングの各ステップの後に実行しようとした場合は、アプリケーション全体が数回加速していることを視覚的に確認することさえできるはずです。

データ変換メソッドを作成する

City Country (都市とこの都市が位置する国に関するデータ) とLanguage (言語に関するデータ) のcom.codegym.redis2 つのクラスを追加するパッケージを作成しましょう。ここでは、「ブレーキ要求」の「タスク別」でよく要求されるすべてのフィールドを示します。

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

この方法は一目瞭然だと思います。単にCity Countryエンティティを作成し、それにCityCountryCountryLanguageのデータを入力するだけです。

RedisサーバーをDockerコンテナとして実行する

ここには 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 がローカル マシンに転送され、外部クライアントに接続して内部に保存されている内容を確認できることです。したがって、私は意味のある名前を付けていましredis-stackredis

起動後、実行中のコンテナのリストが表示されます。これを行うには、次のコマンドを実行します。

docker container ls 

そして、次のようなものが表示されます。

何らかのコマンドを見つける必要がある場合は、ターミナルのヘルプ (docker ヘルプ) を参照するか、Google で「how to ...」を検索することができます (たとえば、docker how to delete 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();
            }
        }

    }
}

ここでは、大根クライアントとの同期接続が開かれ、City Country型の各オブジェクトが順次大根に書き込まれます。大根はStringキー/値ストアであるため、キー (都市 ID) は文字列に変換されます。値も文字列になりますが、 JSON 形式のObjectMapperを使用します。

引き続き実行して、ログにエラーがないことを確認します。すべてがうまくいきました。

redis-insight をインストールし、Redis に保存されているデータを確認します (オプション)

リンクから redis-insight をダウンロードし てインストールします。開始すると、すぐに docker コンテナーに大根インスタンスが表示されます。

ログインすると、すべてのキーのリストが表示されます。

そして、任意のキーに移動して、そこに保存されているデータを確認できます。

Redis からデータを取得するメソッドを作成する

テストには、次のテストを使用します。10 個の Citycountry レコードを取得します。それぞれが別個のリクエストを持ちますが、接続は 1 つです。

大根からのデータは、大根クライアントを通じて取得できます。これを行うには、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を取得し、それを必要なCity Countryタイプのオブジェクトに変換します。

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 の同様のメソッドを 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();
    }
}

機能のうち、完全なオブジェクト (プロキシ スタブなし) を確実に取得するために、国から言語のリストを明示的にリクエストします。

MySQL と Redis から同じデータを取得する速度を比較する

ここでは、メイン メソッドのコードと、ローカル コンピューターで得られる結果をすぐに示します。

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から、アプリケーションを実行します。

私のテスト結果は次のとおりです。

トータルで「ブレーキ頻繁」要求への対応性能は1.5倍向上しました。これは、テストでObjectMapperを介した最速の逆シリアル化を使用しなかったという事実を考慮しています。これを GSON に変更すると、おそらくもう少し時間が「勝つ」ことができます。

この瞬間、私はプログラマーと時間についてのジョークを思い出します。コードを書いて最適化する方法を読んで考えてください。