Hari ini kita akan mengerjakan tugas akhir pada modul JRU keempat. Akan apa? Mari kita coba bekerja dengan teknologi yang berbeda: MySQL, Hibernate, Redis, Docker. Sekarang lebih banyak subjek.

Tugas: kami memiliki database MySQL relasional dengan skema (negara-kota, bahasa per negara). Dan sering ada permintaan dari kota, yang melambat. Kami menemukan solusi - untuk memindahkan semua data yang sering diminta ke Redis (dalam penyimpanan memori dari jenis nilai kunci).

Dan kami tidak membutuhkan semua data yang disimpan di MySQL, tetapi hanya kumpulan bidang yang dipilih. Proyek ini akan dalam bentuk tutorial. Artinya disini kita akan mengangkat masalah tersebut dan segera menyelesaikannya.

Jadi, mari kita mulai dengan perangkat lunak apa yang kita perlukan:

  1. IDEA Ultimate (yang kehabisan kunci - tulis ke Roman in the Slack)
  2. Meja kerja (atau klien lain untuk MySQL)
  3. Buruh pelabuhan
  4. redis-insight - opsional

Rencana aksi kami:

  1. Siapkan docker (Saya tidak akan melakukan ini di tutorial, karena setiap OS akan memiliki karakteristiknya sendiri dan ada banyak jawaban di Internet untuk pertanyaan seperti "cara menginstal docker di windows"), periksa apakah semuanya berfungsi.
  2. Jalankan server MySQL sebagai wadah buruh pelabuhan.
  3. Perluas tempat pembuangan .
  4. Buat proyek di Idea, tambahkan dependensi maven.
  5. Buat domain lapisan.
  6. Tulis metode untuk mendapatkan semua data dari MySQL.
  7. Tulis metode transformasi data (di Redis kami hanya akan menulis data yang sering diminta).
  8. Jalankan server Redis sebagai wadah buruh pelabuhan.
  9. Tulis data ke Redis.
  10. Opsional: instal redis-insight, lihat data yang tersimpan di Redis.
  11. Tulis metode untuk mendapatkan data dari Redis.
  12. Tulis metode untuk mendapatkan data dari MySQL.
  13. Bandingkan kecepatan mendapatkan data yang sama dari MySQL dan Redis.

Pengaturan Docker

Docker adalah platform terbuka untuk mengembangkan, mengirim, dan mengoperasikan aplikasi. Kami akan menggunakannya agar tidak menginstal dan mengonfigurasi Redis di mesin lokal, tetapi menggunakan image yang sudah jadi. Anda dapat membaca lebih lanjut tentang buruh pelabuhan di sini atau melihatnya di sini . Jika Anda tidak terbiasa dengan buruh pelabuhan, saya sarankan untuk melihat tautan kedua saja.

Untuk memastikan Anda telah menginstal dan mengkonfigurasi docker, jalankan perintah:docker -v

Jika semuanya baik-baik saja, Anda akan melihat versi buruh pelabuhan

Jalankan server MySQL sebagai wadah buruh pelabuhan

Agar dapat membandingkan waktu pengembalian data dari MySQL dan Redis, kami juga akan menggunakan MySQL di docker. Di PowerShell (atau terminal konsol lain jika Anda tidak menggunakan Windows), jalankan perintah:

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

Pertimbangkan apa yang kita lakukan dengan perintah ini:

  • docker run– meluncurkan (dan mengunduh, jika belum diunduh ke mesin lokal) image. Sebagai hasil dari peluncuran, kami mendapatkan wadah yang sedang berjalan.
  • --name mysql- atur nama wadah mysql.
  • -d- bendera yang menyatakan bahwa penampung harus terus bekerja, bahkan jika Anda menutup jendela terminal tempat penampung ini diluncurkan.
  • -p 3306:3306- menentukan port. Sebelum titik dua - port pada mesin lokal, setelah titik dua - port dalam wadah.
  • -e MYSQL_ROOT_PASSWORD=root– meneruskan variabel lingkungan MYSQL_ROOT_PASSWORD dengan nilai root ke container. Tandai khusus untuk mysql/ image
  • --restart unless-stopped- mengatur kebijakan perilaku (apakah wadah harus dimulai ulang saat ditutup). Nilai kecuali-berhenti berarti selalu memulai kembali, kecuali ketika wadah dihentikan /
  • -v mysql:/var/lib/mysql – tambahkan volume (gambar untuk menyimpan informasi).
  • mysql:8 – nama gambar dan versinya.

Setelah menjalankan perintah di terminal, buruh pelabuhan akan mengunduh semua lapisan gambar dan memulai wadah:

Catatan penting: jika Anda menginstal MySQL sebagai layanan di komputer lokal Anda dan sedang berjalan, Anda perlu menentukan port yang berbeda di perintah mulai, atau hentikan layanan yang sedang berjalan ini.

Perluas tempat pembuangan

Untuk memperluas dump, Anda perlu membuat koneksi baru ke database dari Workbench, tempat Anda menentukan parameternya. Saya menggunakan port default (3306), tidak mengubah nama pengguna (root secara default) dan mengatur kata sandi untuk pengguna root (root).

Di Workbench, lakukan Data Import/Restoredan pilih Import from Self Contained File. Tentukan tempat Anda mengunduh dump sebagai file . Anda tidak perlu membuat skema sebelumnya - pembuatannya sudah termasuk dalam file dump. Setelah impor berhasil, Anda akan memiliki skema dunia dengan tiga tabel:

  1. kota adalah tabel kota.
  2. negara - tabel negara.
  3. country_language - tabel yang menunjukkan persentase populasi di negara tersebut yang berbicara bahasa tertentu.

Karena kami menggunakan volume saat memulai penampung, setelah menghentikan dan bahkan menghapus penampung mysql dan menjalankan kembali perintah mulai ( docker run --name mysql -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root --restart unless-stopped -v mysql:/var/lib/mysql mysql:8), tidak perlu menerapkan dump lagi - ini sudah diterapkan dalam volume.

Buat proyek di Idea, tambahkan dependensi maven

Anda sudah tahu cara membuat proyek di Ide - ini adalah poin termudah dalam proyek hari ini.

Tambahkan dependensi ke file 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> 

Tiga dependensi pertama sudah lama Anda kenal.

lettuce-coreadalah salah satu klien Java yang tersedia untuk bekerja dengan Redis.

jackson-databind– ketergantungan untuk menggunakan ObjectMapper (untuk mengubah data untuk penyimpanan di Redis (nilai kunci dari tipe String)).

Juga di folder sumber daya (src/main/resources) tambahkan spy.properties untuk melihat permintaan dengan parameter yang dijalankan Hibernate. Isi berkas:

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 

Buat domain lapisan

Buat paket com.codegym.domain

Lebih mudah bagi saya saat memetakan tabel pada entitas untuk menggunakan struktur tabel di Idea, jadi mari tambahkan koneksi database di Idea.

Saya menyarankan untuk membuat entitas dalam urutan ini:

  • Negara
  • Kota
  • Bahasa Negara

Sebaiknya Anda melakukan pemetaan sendiri.

Kode kelas negara:

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

}

Ada 3 poin menarik dalam kode tersebut.

Yang pertama adalah Continent enam , yang disimpan dalam database sebagai nilai ordinal. Dalam struktur tabel negara, di kolom komentar ke benua, Anda dapat melihat nilai numerik mana yang sesuai dengan benua mana.

package com.codegym.domain;

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

Poin kedua adalah sekumpulan entitasCountryLanguage . Berikut tautan @OneToManyyang tidak ada di draf kedua modul ini. Secara default, Hibernasi tidak akan menarik nilai set ini saat meminta entitas negara. Tapi karena kita perlu mengurangi semua nilai dari database relasional untuk caching, file FetchType.EAGER.

Yang ketiga adalah bidang kota . Komunikasi @OneToOne- seperti semuanya akrab dan dapat dimengerti. Namun, jika kita melihat struktur kunci asing di database, kita melihat bahwa negara (negara) memiliki tautan ke ibu kota (kota), dan kota (kota) memiliki tautan ke negara (negara). Ada hubungan siklus.

Kami belum akan melakukan apa pun dengan ini, tetapi ketika kami sampai ke item "Tulis metode untuk mendapatkan semua data dari MySQL", mari kita lihat kueri apa yang dijalankan Hibernate, lihat nomornya, dan ingat item ini.

Kode kelas kota:

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

}

Kode kelas 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
}

Tulis metode untuk mendapatkan semua data dari MySQL

Di kelas Utama, kami mendeklarasikan bidang:

private final SessionFactory sessionFactory;
private final RedisClient redisClient;

private final ObjectMapper mapper;

private final CityDAO cityDAO;
private final CountryDAO countryDAO;

dan menginisialisasinya di konstruktor kelas Utama:

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

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

Seperti yang Anda lihat, metode dan kelas tidak cukup - mari kita tulis.

Deklarasikan paket com.codegym.dao dan tambahkan 2 kelas ke dalamnya:

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

Sekarang Anda dapat mengimpor 2 kelas ini ke Main. Masih kehilangan dua metode:

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

Kami belum mencapai lobak, jadi penerapan inisialisasi klien lobak akan tetap menjadi rintisan untuk saat ini:

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

Terakhir, kita dapat menulis sebuah metode di mana kita menarik semua kota:

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

Fitur implementasi sedemikian rupa sehingga kami mendapatkan masing-masing 500 kota. Ini diperlukan karena ada batasan jumlah data yang dikirimkan. Ya, dalam kasus kami, kami tidak akan mendapatkannya, karena. kami memiliki total 4079 kota di database. Namun dalam aplikasi produksi, ketika Anda membutuhkan banyak data, teknik ini sering digunakan.

Dan penerapan metode utama:

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

Sekarang kita dapat menjalankan aplikasi kita di debug untuk pertama kalinya dan melihat cara kerjanya (atau tidak berfungsi - ya, itu sering terjadi).

Kota-kota semakin. Setiap kota mendapat negara, jika sebelumnya tidak dikurangkan dari basis data untuk kota lain. Mari kita hitung secara kasar berapa banyak kueri yang akan dikirim Hibernate ke database:

  • 1 permintaan untuk mengetahui jumlah total kota (diperlukan untuk mengulangi lebih dari 500 kota untuk mengetahui kapan harus berhenti).
  • 4079 / 500 = 9 permintaan (daftar kota).
  • Setiap kota mendapat negara, jika belum dikurangi sebelumnya. Karena ada 239 negara dalam database, ini akan memberi kita 239 kueri.

Total 249 requests. Dan kami juga mengatakan bahwa bersama dengan negara kami akan segera menerima satu set bahasa, jika tidak maka akan ada kegelapan secara umum. Tapi itu masih banyak, jadi mari kita ubah sedikit perilakunya. Mari kita mulai dengan refleksi: apa yang harus dilakukan, ke mana harus lari? Tapi serius - mengapa ada begitu banyak permintaan. Jika Anda melihat log permintaan, kami melihat bahwa setiap negara diminta secara terpisah, jadi solusi sederhana pertama: mari kita meminta semua negara secara bersamaan, karena kami tahu sebelumnya bahwa kami akan membutuhkan semuanya dalam transaksi ini.

Dalam metode fetchData(), segera setelah dimulainya transaksi, tambahkan baris berikut:

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

Kami menghitung permintaan:

  • 1 - dapatkan semua negara
  • 239 - permintaan untuk setiap negara ibukotanya
  • 1 - permintaan jumlah kota
  • 9 - permintaan daftar kota

Total 250. Idenya bagus, tapi tidak berhasil. Soalnya negara itu punya koneksi dengan ibu kota (kota) @OneToOne. Dan tautan semacam itu langsung dimuat secara default ( FetchType.EAGER). Mari kita menempatkan FetchType.LAZY, karena bagaimanapun, kami akan memuat semua kota nanti dalam transaksi yang sama.

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

Kapital tidak lagi diminta secara terpisah, tetapi jumlah permintaan tidak berubah. Sekarang, untuk setiap negara, daftar CountryLanguage diminta oleh kueri terpisah . Artinya, ada kemajuan, dan kami bergerak ke arah yang benar. Jika Anda ingat, kuliah menyarankan solusi "join fetch" untuk meminta entitas dengan data dependen dalam satu permintaan dengan menambahkan gabungan tambahan ke permintaan tersebut. Di CountryDAO , tulis ulang kueri HQL dalam metode getAll()untuk:

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

Meluncurkan. Kami melihat log, menghitung permintaan:

  • 1 - semua negara dengan bahasa
  • 1 - jumlah kota
  • 9 - daftar kota.

Total 11- kami berhasil)) Jika Anda tidak hanya membaca semua teks ini, tetapi juga mencoba menjalankannya setelah setiap langkah penyetelan aplikasi, Anda bahkan harus mencatat secara visual percepatan seluruh aplikasi beberapa kali.

Tulis metode transformasi data

Mari buat paket com.codegym.redisdi mana kita menambahkan 2 kelas: CityCountry (data tentang kota dan negara tempat kota ini berada) dan Bahasa (data tentang bahasa). Berikut adalah semua bidang yang sering diminta "berdasarkan tugas" di "permintaan pengereman".

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
}

Dalam metode utama, setelah mendapatkan semua kota, tambahkan baris

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

Dan terapkan metode ini:

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

Saya pikir metode ini cukup jelas: kita hanya membuat entitas CityCountry dan mengisinya dengan data dari City , Country , CountryLanguage .

Jalankan server Redis sebagai wadah buruh pelabuhan

Ada 2 opsi di sini. Jika Anda melakukan langkah opsional "instal redis-insight, lihat data yang disimpan di Redis", maka perintahnya untuk Anda:

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

Jika Anda memutuskan untuk melewati langkah ini, cukup:

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

Perbedaannya adalah pada opsi pertama, port 8001 diteruskan ke mesin lokal, yang dapat Anda sambungkan dengan klien eksternal untuk melihat apa yang disimpan di dalamnya. Dan saya dulu memberi nama yang bermakna, oleh karena itu, redis-stackatau redis.

Setelah diluncurkan, Anda dapat melihat daftar wadah yang sedang berjalan. Untuk melakukan ini, jalankan perintah:

docker container ls 

Dan Anda akan melihat sesuatu seperti ini:

Jika Anda perlu menemukan beberapa perintah, Anda dapat melihat bantuan di terminal (bantuan buruh pelabuhan) atau google "cara ..." (misalnya, buruh pelabuhan cara menghapus wadah yang sedang berjalan).

Dan kami juga memanggil inisialisasi klien lobak di konstruktor Utama, tetapi tidak mengimplementasikan metode itu sendiri. Tambahkan implementasi:

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 ditambahkan untuk tujuan pendidikan sehingga di log peluncuran Anda dapat melihat bahwa semuanya baik-baik saja dan koneksi melalui klien lobak berlalu tanpa kesalahan.

Tulis data ke Redis

Tambahkan panggilan ke metode utama

main.pushToRedis(preparedData); 

Dengan implementasi metode ini:

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

    }
}

Di sini, koneksi sinkron dibuka dengan klien lobak, dan secara berurutan setiap objek tipe CityCountry ditulis ke lobak. Karena lobak adalah String key-value store , kunci (city id) diubah menjadi string. Dan nilainya juga ke string, tetapi menggunakan ObjectMapper dalam format JSON.

Tetap berjalan dan periksa apakah tidak ada kesalahan di log. Semuanya bekerja.

Instal redis-insight, lihat data yang disimpan di Redis (opsional)

Unduh redis-insight dari tautan dan instal. Setelah memulai, ini segera menampilkan instance lobak kami di wadah buruh pelabuhan:

Jika Anda masuk, kami akan melihat daftar semua kunci:

Dan Anda dapat membuka kunci apa saja untuk melihat data apa yang disimpan di dalamnya:

Tulis metode untuk mendapatkan data dari Redis

Untuk pengujian, kami menggunakan pengujian berikut: kami mendapatkan 10 catatan CityCountry. Masing-masing dengan permintaan terpisah, tetapi dalam satu koneksi.

Data dari lobak dapat diperoleh melalui klien lobak kami. Untuk melakukan ini, mari tulis metode yang mengambil daftar id untuk mendapatkannya.

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

Implementasinya, menurut saya, intuitif: kami membuka koneksi sinkron, dan untuk setiap id kami mendapatkan JSON String , yang kami ubah menjadi objek tipe CityCountry yang kami butuhkan .

Tulis metode untuk mendapatkan data dari MySQL

Di kelas CityDAO , tambahkan metode getById(Integer id)di mana kita akan mendapatkan kota beserta negaranya:

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

Dengan analogi paragraf sebelumnya, mari tambahkan metode serupa untuk MySQL ke kelas Utama:

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

Dari fitur-fiturnya, untuk memastikan mendapatkan objek lengkap (tanpa rintisan proxy), kami secara eksplisit meminta daftar bahasa dari negara tersebut.

Bandingkan kecepatan mendapatkan data yang sama dari MySQL dan Redis

Di sini saya akan langsung memberikan kode metode utama, dan hasil yang didapat di komputer lokal saya.

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

Saat pengujian, ada fitur - data dari MySQL hanya dibaca, sehingga tidak dapat dimulai ulang di antara peluncuran aplikasi kita. Dan di Redis mereka ditulis.

Meskipun saat Anda mencoba menambahkan duplikat untuk kunci yang sama, data hanya akan diperbarui, saya sarankan Anda menjalankan perintah untuk menghentikan wadah docker stop redis-stackdan menghapus wadah di antara peluncuran aplikasi di terminal docker rm redis-stack. Setelah itu, angkat kembali wadah berisi lobak docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:latestdan baru setelah itu jalankan aplikasi kita.

Berikut adalah hasil tes saya:

Secara total, kami telah mencapai peningkatan kinerja respons terhadap permintaan "pengereman yang sering" sebanyak satu setengah kali. Dan ini memperhitungkan fakta bahwa dalam pengujian kami tidak menggunakan deserialisasi tercepat melalui ObjectMapper . Jika Anda mengubahnya ke GSON, kemungkinan besar Anda bisa "memenangkan" lebih banyak waktu.

Saat ini, saya ingat lelucon tentang programmer dan waktu: baca dan pikirkan tentang cara menulis dan mengoptimalkan kode Anda.