Hari ini kami akan membuat projek akhir modul JRU yang keempat. Apakah yang akan berlaku? Mari cuba bekerja dengan teknologi yang berbeza: MySQL, Hibernate, Redis, Docker. Sekarang lebih banyak subjek.

Tugas: kami mempunyai pangkalan data MySQL hubungan dengan skema (negara-bandar, bahasa mengikut negara). Dan terdapat permintaan yang kerap dari bandar, yang perlahan. Kami menghasilkan penyelesaian - untuk mengalihkan semua data yang sering diminta ke Redis (dalam simpanan memori jenis nilai kunci).

Dan kita tidak memerlukan semua data yang disimpan dalam MySQL, tetapi hanya satu set medan yang dipilih. Projek itu akan dalam bentuk tutorial. Maksudnya, di sini kami akan membangkitkan masalah dan segera menyelesaikannya.

Jadi, mari kita mulakan dengan perisian yang kita perlukan:

  1. IDEA Ultimate (yang kehabisan kunci - tulis kepada Roman dalam Slack)
  2. Meja kerja (atau mana-mana pelanggan lain untuk MySQL)
  3. Docker
  4. redis-insight - pilihan

Pelan tindakan kami:

  1. Sediakan docker (saya tidak akan melakukan ini dalam tutorial, kerana setiap OS akan mempunyai ciri-cirinya sendiri dan terdapat banyak jawapan di Internet untuk soalan seperti "cara memasang docker pada windows"), pastikan semuanya berfungsi.
  2. Jalankan pelayan MySQL sebagai bekas docker.
  3. Kembangkan tempat pembuangan .
  4. Buat projek dalam Idea, tambah kebergantungan maven.
  5. Buat domain lapisan.
  6. Tulis kaedah untuk mendapatkan semua data daripada MySQL.
  7. Tulis kaedah transformasi data (dalam Redis kami akan menulis hanya data yang sering diminta).
  8. Jalankan pelayan Redis sebagai bekas docker.
  9. Tulis data kepada Redis.
  10. Pilihan: pasang redis-insight, lihat data yang disimpan dalam Redis.
  11. Tulis kaedah untuk mendapatkan data daripada Redis.
  12. Tulis kaedah untuk mendapatkan data daripada MySQL.
  13. Bandingkan kelajuan mendapatkan data yang sama daripada MySQL dan Redis.

Persediaan Docker

Docker ialah platform terbuka untuk membangunkan, menyampaikan dan mengendalikan aplikasi. Kami akan menggunakannya untuk tidak memasang dan mengkonfigurasi Redis pada mesin tempatan, tetapi untuk menggunakan imej siap pakai. Anda boleh membaca lebih lanjut mengenai docker di sini atau lihat di sini . Jika anda tidak biasa dengan docker, saya syorkan anda hanya melihat pautan kedua.

Untuk memastikan anda memasang dan mengkonfigurasi docker, jalankan arahan:docker -v

Jika semuanya OK, anda akan melihat versi docker

Jalankan pelayan MySQL sebagai bekas docker

Untuk dapat membandingkan masa pemulangan data dari MySQL dan Redis, kami juga akan menggunakan MySQL dalam docker. Dalam PowerShell (atau terminal konsol lain jika anda tidak menggunakan Windows), jalankan arahan:

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 arahan ini:

  • docker run– melancarkan (dan memuat turun, jika ia belum lagi dimuat turun ke mesin tempatan) imej. Hasil daripada pelancaran, kami mendapat bekas yang sedang berjalan.
  • --name mysql- tetapkan nama bekas mysql.
  • -d- bendera yang mengatakan bahawa bekas harus terus berfungsi, walaupun anda menutup tetingkap terminal dari mana bekas ini dilancarkan.
  • -p 3306:3306- menentukan port. Sebelum kolon - port pada mesin tempatan, selepas kolon - port dalam bekas.
  • -e MYSQL_ROOT_PASSWORD=root– menghantar pembolehubah persekitaran MYSQL_ROOT_PASSWORD dengan punca nilai ke bekas. Benderakan khusus untuk imej mysql/
  • --restart unless-stopped- menetapkan dasar tingkah laku (sama ada bekas perlu dimulakan semula apabila ditutup). Nilai kecuali dihentikan bermaksud sentiasa dimulakan semula, kecuali apabila bekas dihentikan /
  • -v mysql:/var/lib/mysql – tambah kelantangan (imej untuk menyimpan maklumat).
  • mysql:8 – nama imej dan versinya.

Selepas melaksanakan arahan dalam terminal, docker akan memuat turun semua lapisan imej dan memulakan bekas:

Nota penting: jika anda memasang MySQL sebagai perkhidmatan pada komputer tempatan anda dan ia sedang berjalan, anda perlu menentukan port lain dalam arahan mula, atau hentikan perkhidmatan berjalan ini.

Kembangkan tempat pembuangan

Untuk mengembangkan pembuangan, anda perlu membuat sambungan baharu ke pangkalan data daripada Workbench, di mana anda menentukan parameter. Saya menggunakan port lalai (3306), tidak menukar nama pengguna (root secara lalai), dan tetapkan kata laluan untuk pengguna root (root).

Dalam Meja Kerja, lakukan Data Import/Restoredan pilih Import from Self Contained File. Nyatakan tempat anda memuat turun dump sebagai fail . Anda tidak perlu mencipta skema terlebih dahulu - penciptaannya disertakan dalam fail dump. Selepas import berjaya, anda akan mempunyai skema dunia dengan tiga jadual:

  1. bandar ialah jadual bandar.
  2. negara - meja negara.
  3. bahasa_negara - jadual yang menunjukkan berapa peratus penduduk di negara itu bercakap bahasa tertentu.

Memandangkan kami menggunakan kelantangan semasa memulakan bekas, selepas menghentikan dan juga memadamkan bekas mysql dan melaksanakan semula arahan mula ( 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 menggunakan dump semula - ia sudah digunakan dalam kelantangan.

Buat projek dalam Idea, tambah kebergantungan maven

Anda sudah tahu cara membuat projek dalam Idea - ini adalah titik paling mudah dalam projek hari ini.

Tambahkan kebergantungan pada fail 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 kebergantungan pertama telah lama anda kenali.

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

jackson-databind– pergantungan untuk menggunakan ObjectMapper (untuk mengubah data untuk penyimpanan dalam Redis (nilai kunci jenis String)).

Juga dalam folder sumber (src/main/resources) tambah spy.properties untuk melihat permintaan dengan parameter yang Hibernate laksanakan. Kandungan fail:

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

Cipta pakej com.codegym.domain

Adalah mudah bagi saya apabila memetakan jadual pada entiti untuk menggunakan struktur jadual dalam Idea, jadi mari tambah sambungan pangkalan data dalam Idea.

Saya cadangkan membuat entiti dalam susunan ini:

  • Negara
  • bandar
  • Bahasa Negara

Adalah wajar anda melakukan pemetaan sendiri.

Kod 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

}

Terdapat 3 perkara menarik dalam kod tersebut.

Yang pertama ialah enam Continent , yang disimpan dalam pangkalan data sebagai nilai ordinal. Dalam struktur jadual negara, dalam ulasan ke medan benua, anda boleh melihat nilai berangka yang sepadan dengan benua mana.

package com.codegym.domain;

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

Perkara kedua ialah satu set entitiCountryLanguage . Berikut ialah pautan @OneToManyyang tiada dalam draf kedua modul ini. Secara lalai, Hibernate tidak akan menarik nilai set ini apabila meminta entiti negara. Tetapi kerana kita perlu menolak semua nilai daripada pangkalan data hubungan untuk caching, FetchType.EAGER.

Yang ketiga ialah padang bandar . Komunikasi @OneToOne- seperti segala-galanya biasa dan boleh difahami. Tetapi, jika kita melihat struktur kunci asing dalam pangkalan data, kita melihat bahawa negara (negara) mempunyai pautan ke ibu kota (bandar), dan bandar (bandar) mempunyai pautan ke negara (negara). Terdapat hubungan kitaran.

Kami tidak akan melakukan apa-apa lagi dengan ini, tetapi apabila kami sampai ke item "Tulis kaedah untuk mendapatkan semua data daripada MySQL", mari lihat pertanyaan apa yang Hibernate laksanakan, lihat nombornya dan ingat item ini.

Kod kelas bandar:

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

}

Kod 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 kaedah untuk mendapatkan semua data daripada MySQL

Dalam kelas Utama, kami mengisytiharkan medan:

private final SessionFactory sessionFactory;
private final RedisClient redisClient;

private final ObjectMapper mapper;

private final CityDAO cityDAO;
private final CountryDAO countryDAO;

dan mulakannya dalam pembina kelas Utama:

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

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

Seperti yang anda lihat, kaedah dan kelas tidak mencukupi - mari kita tuliskannya.

Isytiharkan pakej com.codegym.dao dan tambahkan 2 kelas padanya:

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

Kini anda boleh mengimport 2 kelas ini ke dalam Utama. Masih kehilangan dua kaedah:

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 pelaksanaan pemulaan pelanggan lobak akan kekal sebagai rintisan buat masa ini:

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

Akhirnya, kita boleh menulis kaedah di mana kita menarik keluar semua bandar:

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

Ciri pelaksanaan adalah sedemikian rupa sehingga kami mendapat 500 bandar setiap satu. Ini perlu kerana terdapat sekatan pada jumlah data yang dihantar. Ya, dalam kes kita, kita tidak akan sampai kepada mereka, kerana. kami mempunyai sejumlah 4079 bandar dalam pangkalan data. Tetapi dalam aplikasi pengeluaran, apabila anda perlu mendapatkan banyak data, teknik ini sering digunakan.

Dan pelaksanaan kaedah utama:

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

Kini kami boleh menjalankan aplikasi kami dalam nyahpepijat buat kali pertama dan melihat cara ia berfungsi (atau tidak berfungsi - ya, ia sering berlaku).

Bandar semakin mendapat. Setiap bandar mendapat sebuah negara, jika ia belum pernah ditolak daripada pangkalan data untuk bandar lain. Mari kita kira secara kasar berapa banyak pertanyaan yang akan dihantar oleh Hibernate ke pangkalan data:

  • 1 permintaan untuk mengetahui jumlah bandar (perlu mengulangi lebih 500 bandar untuk mengetahui masa untuk berhenti).
  • 4079 / 500 = 9 permintaan (senarai bandar).
  • Setiap bandar mendapat sebuah negara, jika ia tidak ditolak lebih awal. Memandangkan terdapat 239 negara dalam pangkalan data, ini akan memberi kami 239 pertanyaan.

Total 249 requests. Dan kami juga berkata bahawa bersama-sama dengan negara itu kami akan segera menerima satu set bahasa, jika tidak akan ada kegelapan secara umum. Tetapi ia masih banyak, jadi mari kita tweak sedikit tingkah laku. Mari kita mulakan dengan refleksi: apa yang perlu dilakukan, di mana hendak lari? Tetapi serius - mengapa terdapat begitu banyak permintaan. Jika anda melihat log permintaan, kami melihat bahawa setiap negara diminta secara berasingan, jadi penyelesaian mudah pertama: mari kita minta semua negara bersama-sama, kerana kami tahu terlebih dahulu bahawa kami memerlukan kesemuanya dalam transaksi ini.

Dalam kaedah fetchData(), sejurus selepas permulaan transaksi, tambah baris berikut:

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

Kami mengira permintaan:

  • 1 - dapatkan semua negara
  • 239 - pertanyaan untuk setiap negara ibu kotanya
  • 1 - permintaan untuk bilangan bandar
  • 9 - permintaan untuk senarai bandar

Total 250. Ideanya bagus, tetapi tidak berjaya. Masalahnya ialah negara mempunyai kaitan dengan ibu kota (bandar) @OneToOne. Dan pautan sedemikian dimuatkan serta-merta secara lalai ( FetchType.EAGER). Mari kita letakkan FetchType.LAZY, kerana Bagaimanapun, kami akan memuatkan semua bandar kemudian dalam transaksi yang sama.

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

Modal tidak lagi diminta secara berasingan, tetapi bilangan permintaan tidak berubah. Kini, untuk setiap negara, senarai CountryLanguage diminta oleh pertanyaan berasingan . Iaitu, terdapat kemajuan, dan kita bergerak ke arah yang betul. Jika anda masih ingat, kuliah mencadangkan penyelesaian "mengambil sertai" untuk meminta entiti dengan data bergantung dalam satu permintaan dengan menambahkan gabungan tambahan pada permintaan itu. Dalam CountryDAO , tulis semula pertanyaan HQL dalam kaedah getAll()untuk:

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

Pelancaran. Kami melihat log, mengira permintaan:

  • 1 - semua negara dengan bahasa
  • 1 - bilangan bandar
  • 9 - senarai bandar.

Total 11- kami berjaya)) Jika anda bukan sahaja membaca semua teks ini, tetapi juga cuba menjalankannya selepas setiap langkah penalaan aplikasi, anda juga harus perhatikan secara visual pecutan keseluruhan aplikasi beberapa kali.

Tulis kaedah transformasi data

Mari buat pakej com.codegym.redisyang mana kami menambah 2 kelas: CityCountry (data tentang bandar dan negara tempat bandar ini terletak) dan Bahasa (data tentang bahasa). Berikut adalah semua medan yang sering diminta "oleh tugas" dalam "permintaan brek".

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 kaedah utama, selepas mendapat semua bandar, tambah baris

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

Dan laksanakan kaedah 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 rasa kaedah ini cukup jelas: kami hanya mencipta entiti CityCountry dan mengisinya dengan data daripada City , Country , CountryLanguage .

Jalankan pelayan Redis sebagai bekas docker

Terdapat 2 pilihan di sini. Jika anda melakukan langkah pilihan "pasang redis-insight, lihat data yang disimpan dalam Redis", maka arahannya adalah untuk anda:

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

Jika anda memutuskan untuk melangkau langkah ini, maka hanya:

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

Perbezaannya ialah dalam pilihan pertama, port 8001 dimajukan ke mesin tempatan, yang mana anda boleh berhubung dengan pelanggan luaran untuk melihat apa yang disimpan di dalamnya. Dan saya pernah memberikan nama yang bermakna, oleh itu, redis-stackatau redis.

Selepas pelancaran, anda boleh melihat senarai bekas yang sedang berjalan. Untuk melakukan ini, jalankan arahan:

docker container ls 

Dan anda akan melihat sesuatu seperti ini:

Jika anda perlu mencari beberapa arahan, anda boleh melihat sama ada pada bantuan terminal (bantuan buruh pelabuhan) atau google "bagaimana untuk ..." (contohnya, buruh pelabuhan cara mengalih keluar bekas yang sedang berjalan).

Dan kami juga memanggil pemulaan pelanggan lobak dalam pembina Utama, tetapi tidak melaksanakan kaedah itu sendiri. Tambah pelaksanaan:

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 telah ditambah untuk tujuan pendidikan supaya dalam log pelancaran anda dapat melihat bahawa semuanya OK dan sambungan melalui pelanggan lobak diluluskan tanpa ralat.

Tulis data kepada Redis

Tambahkan panggilan ke kaedah utama

main.pushToRedis(preparedData); 

Dengan pelaksanaan kaedah 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, sambungan segerak dibuka dengan pelanggan lobak, dan secara berurutan setiap objek jenis CityCountry ditulis pada lobak. Memandangkan lobak ialah kedai nilai kunci String , kunci (id bandar) ditukar kepada rentetan. Dan nilainya juga kepada rentetan, tetapi menggunakan ObjectMapper dalam format JSON.

Ia kekal untuk menjalankan dan menyemak bahawa tiada ralat dalam log. Semuanya berjaya.

Pasang redis-insight, lihat data yang disimpan dalam Redis (pilihan)

Muat turun redis-insight daripada pautan dan pasangkannya. Selepas bermula, ia segera menunjukkan contoh lobak kami dalam bekas docker:

Jika anda log masuk, kami akan melihat senarai semua kunci:

Dan anda boleh pergi ke mana-mana kunci untuk melihat data yang disimpan padanya:

Tulis kaedah untuk mendapatkan data daripada Redis

Untuk ujian, kami menggunakan ujian berikut: kami mendapat 10 rekod CityCountry. Masing-masing dengan permintaan yang berasingan, tetapi dalam satu sambungan.

Data daripada lobak boleh didapati melalui pelanggan lobak kami. Untuk melakukan ini, mari tulis kaedah yang memerlukan senarai id untuk diperoleh.

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

Pelaksanaan, saya fikir, adalah intuitif: kami membuka sambungan segerak, dan untuk setiap id kami mendapat JSON String , yang kami tukar menjadi objek jenis CityCountry yang kami perlukan .

Tulis kaedah untuk mendapatkan data daripada MySQL

Dalam kelas CityDAO , tambahkan kaedah getById(Integer id)di mana kita akan mendapatkan bandar bersama negara:

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 dengan perenggan sebelumnya, mari tambahkan kaedah yang sama 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();
    }
}

Daripada ciri, untuk memastikan untuk mendapatkan objek penuh (tanpa proksi stub), kami secara eksplisit meminta senarai bahasa dari negara itu.

Bandingkan kelajuan mendapatkan data yang sama daripada MySQL dan Redis

Di sini saya akan segera memberikan kod kaedah utama, dan hasil yang diperoleh pada komputer tempatan 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();
}

Apabila menguji, terdapat ciri - data dari MySQL hanya dibaca, jadi ia tidak boleh dimulakan semula antara pelancaran aplikasi kami. Dan dalam Redis mereka ditulis.

Walaupun apabila anda cuba menambah pendua untuk kunci yang sama, data hanya akan dikemas kini, saya akan mengesyorkan anda menjalankan arahan untuk menghentikan bekas docker stop redis-stackdan memadamkan bekas antara pelancaran aplikasi dalam terminal docker rm redis-stack. Selepas itu, naikkan bekas dengan lobak sekali lagi docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:latestdan hanya selepas itu laksanakan permohonan kami.

Berikut adalah keputusan ujian saya:

Secara keseluruhan, kami telah mencapai peningkatan dalam prestasi tindak balas kepada permintaan "kerap brek" sebanyak satu setengah kali ganda. Dan ini mengambil kira hakikat bahawa dalam ujian kami tidak menggunakan penyahserialisasian terpantas melalui ObjectMapper . Jika anda menukarnya kepada GSON, kemungkinan besar, anda boleh "menang" sedikit masa lagi.

Pada masa ini, saya masih ingat satu jenaka tentang pengaturcara dan masa: baca dan fikir tentang cara menulis dan mengoptimumkan kod anda.