Bugün dördüncü JRU modülünün final projesini yapacağız. O ne olacak? Farklı teknolojilerle çalışmayı deneyelim: MySQL, Hibernate, Redis, Docker. Şimdi daha fazla konu.

Görev: Şemaya (ülke-şehir, ülkeye göre dil) sahip ilişkisel bir MySQL veri tabanımız var. Ve yavaşlayan şehrin sık sık talebi var. Bir çözüm bulduk - sık sık istenen tüm verileri Redis'e taşımak (anahtar/değer türünde bellek deposunda).

Ve MySQL'de depolanan tüm verilere değil, yalnızca seçilmiş bir dizi alana ihtiyacımız var. Proje eğitim şeklinde olacaktır. Yani burada sorunu gündeme getireceğiz ve hemen çözeceğiz.

Öyleyse, hangi yazılıma ihtiyacımız olacağıyla başlayalım:

  1. IDEA Ultimate (anahtarı biten - Slack'te Roman'a yazın)
  2. Workbench (veya MySQL için başka herhangi bir istemci)
  3. Liman işçisi
  4. yeniden tespit - isteğe bağlı

Eylem planımız:

  1. Docker'ı kurun (bunu eğitimde yapmayacağım, çünkü her işletim sisteminin kendine has özellikleri olacak ve internette “docker'ı pencerelere nasıl kurarım” gibi sorulara pek çok yanıt var), her şeyin çalışıp çalışmadığını kontrol edin.
  2. MySQL sunucusunu bir docker konteyneri olarak çalıştırın.
  3. Dökümü genişletin .
  4. Idea'da bir proje oluşturun, maven bağımlılıkları ekleyin.
  5. Katman etki alanı yapın.
  6. Tüm verileri MySQL'den almak için bir yöntem yazın.
  7. Bir veri dönüştürme yöntemi yazın (Redis'te yalnızca sık istenen verileri yazacağız).
  8. Redis sunucusunu bir docker konteyneri olarak çalıştırın.
  9. Verileri Redis'e yazın.
  10. İsteğe bağlı: redis-insight'ı kurun, Redis'te saklanan verilere bakın.
  11. Redis'ten veri almak için bir yöntem yazın.
  12. MySQL'den veri almak için bir yöntem yazın.
  13. Aynı verileri MySQL ve Redis'ten alma hızını karşılaştırın.

Liman işçisi kurulumu

Docker, uygulamaları geliştirmek, teslim etmek ve çalıştırmak için açık bir platformdur. Redis'i yerel makineye kurup yapılandırmak için değil, hazır bir imaj kullanmak için kullanacağız. Liman işçisi hakkında daha fazla bilgiyi buradan okuyabilir veya burada görebilirsiniz . Docker'a aşina değilseniz, sadece ikinci bağlantıya bakmanızı tavsiye ederim.

Docker'ın kurulu ve yapılandırılmış olduğundan emin olmak için şu komutu çalıştırın:docker -v

Her şey yolundaysa, liman işçisi sürümünü göreceksiniz

MySQL sunucusunu liman konteyneri olarak çalıştırın

MySQL ve Redis'ten veri döndürme zamanını karşılaştırabilmek için docker'da MySQL'i de kullanacağız. PowerShell'de (veya Windows kullanmıyorsanız başka bir konsol terminalinde) şu komutu çalıştırın:

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

Bu komutla ne yaptığımızı düşünün:

  • docker run– görüntüyü başlatma (ve henüz yerel makineye indirilmemişse indirme). Lansman sonucunda çalışan bir kapsayıcı elde ediyoruz.
  • --name mysql- mysql kabının adını ayarlayın.
  • -d- bu konteynerin başlatıldığı terminal penceresini kapatsanız bile konteynerin çalışmaya devam etmesi gerektiğini söyleyen bir bayrak.
  • -p 3306:3306- bağlantı noktalarını belirtir. İki noktadan önce - yerel makinedeki bağlantı noktası, iki noktadan sonra - kapsayıcıdaki bağlantı noktası.
  • -e MYSQL_ROOT_PASSWORD=root– kök değeriyle MYSQL_ROOT_PASSWORD ortam değişkenini kaba geçirme. mysql/resme özel bayrak
  • --restart unless-stopped- davranış politikasının ayarlanması (kapsayıcı kapatıldığında yeniden başlatılıp başlatılmayacağı). Durdurulmadıkça değeri, kabın durdurulduğu durumlar dışında her zaman yeniden başlatılacağı anlamına gelir /
  • -v mysql:/var/lib/mysql – hacim ekleyin (bilgi depolamak için resim).
  • mysql:8 – görüntünün adı ve sürümü.

Komutu terminalde yürüttükten sonra, liman işçisi görüntünün tüm katmanlarını indirecek ve kabı başlatacaktır:

Önemli not: MySQL yerel bilgisayarınızda servis olarak kuruluysa ve çalışıyorsa, start komutunda farklı bir port belirtmeniz veya çalışan bu servisi durdurmanız gerekir.

Dökümü genişlet

Dökümü genişletmek için, parametreleri belirttiğiniz Workbench'ten veritabanına yeni bir bağlantı oluşturmanız gerekir. Varsayılan bağlantı noktasını (3306) kullandım, kullanıcı adını (varsayılan olarak kök) değiştirmedim ve kök kullanıcı (kök) için parola belirledim.

Workbench'te yapın Data Import/Restoreve seçin Import from Self Contained File. Dökümü dosya olarak nereye indirdiğinizi belirtin . Şemayı önceden oluşturmanıza gerek yoktur - şemanın oluşturulması döküm dosyasına dahildir. Başarılı bir içe aktarma işleminden sonra, üç tablo içeren bir dünya şemanız olacaktır:

  1. city ​​bir şehirler tablosudur.
  2. ülke - ülke tablosu.
  3. country_language - ülkedeki nüfusun yüzde kaçının belirli bir dili konuştuğunu gösteren bir tablo.

Kapsayıcıyı başlatırken bir birim kullandığımız için, mysql kapsayıcısını durdurup hatta sildikten ve start komutunu ( ) yeniden çalıştırdıktan sonra docker run --name mysql -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root --restart unless-stopped -v mysql:/var/lib/mysql mysql:8, dökümü yeniden dağıtmaya gerek kalmayacak - zaten birimde konuşlandırılmış durumda.

Idea'da proje oluşturun, maven bağımlılıkları ekleyin

Idea'da bir projeyi nasıl oluşturacağınızı zaten biliyorsunuz - bu, bugünün projesindeki en kolay nokta.

Pom dosyasına bağımlılıklar ekleyin:


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

İlk üç bağımlılık size uzun zamandır aşinadır.

lettuce-coreRedis ile çalışmak için mevcut Java istemcilerinden biridir.

jackson-databind– ObjectMapper kullanma bağımlılığı (verileri Redis'te depolamak için dönüştürmek için (String türünde anahtar/değer çifti)).

Ayrıca, kaynaklar klasörüne (src/main/resources), Hibernate'in yürüttüğü parametrelerle istekleri görüntülemek için spy.properties ekleyin. Dosya içeriği:

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 

Katman etki alanı yap

com.codegym.domain paketini oluştur

Bir varlık üzerindeki tabloları eşlerken Fikir'deki tablo yapısını kullanmak benim için uygun, bu yüzden Fikir'e bir veritabanı bağlantısı ekleyelim.

Varlıkları şu sırayla oluşturmanızı öneririm:

  • Ülke
  • şehir
  • Ülke dili

Eşlemeyi kendiniz gerçekleştirmeniz arzu edilir.

Ülke sınıf kodu:

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

}

Kodda 3 ilginç nokta var.

İlki , veritabanında sıralı değerler olarak depolanan Kıta enam'dır. Ülke tablosunun yapısında kıta alanına yapılan yorumlarda hangi sayısal değerin hangi kıtaya karşılık geldiğini görebilirsiniz.

package com.codegym.domain;

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

İkinci nokta, bir varlıklar kümesidirCountryLanguage . İşte @OneToManybu modülün ikinci taslağında olmayan bir bağlantı. Varsayılan olarak, Hazırda Bekletme, bir ülke varlığı talep edilirken bu kümenin değerini çekmez. Ancak önbelleğe alma için tüm değerleri ilişkisel veritabanından çıkarmamız gerektiğinden, FetchType.EAGER.

Üçüncüsü şehir alanıdır . İletişim @OneToOne- sanki her şey tanıdık ve anlaşılır. Ancak veri tabanındaki yabancı anahtar yapısına bakarsak, ülkenin (ülke) başkente (şehir), şehrin (şehir) ise ülke (ülke) ile bağlantısı olduğunu görürüz. döngüsel bir ilişki vardır.

Bununla henüz bir şey yapmayacağız, ancak “MySQL'den tüm verileri almak için bir yöntem yazın” öğesine geldiğimizde, Hibernate'in hangi sorguları çalıştırdığına bakalım, sayılarına bakalım ve bu öğeyi hatırlayalım.

Şehir sınıfı kodu:

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

}

Ülke Dili sınıf kodu:

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'den tüm verileri almak için bir yöntem yazın

Main sınıfında alanları bildiririz:

private final SessionFactory sessionFactory;
private final RedisClient redisClient;

private final ObjectMapper mapper;

private final CityDAO cityDAO;
private final CountryDAO countryDAO;

ve bunları Ana sınıfın yapıcısında başlatın:

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

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

Gördüğünüz gibi, yeterli yöntem ve sınıf yok - hadi bunları yazalım.

Bir paket com.codegym.dao bildirin ve ona 2 sınıf ekleyin:

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

Artık bu 2 sınıfı Main'e aktarabilirsiniz. Hala iki yöntem eksik:

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

Turpa henüz ulaşmadık, bu nedenle turp istemcisinin başlatılmasının uygulanması şimdilik bir taslak olarak kalacak:

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

Son olarak, tüm şehirleri çıkardığımız bir yöntem yazabiliriz:

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

Uygulama özelliği, her biri 500 şehir alacak şekildedir. Aktarılan veri miktarında kısıtlamalar olduğu için bu gereklidir. Evet, bizim durumumuzda onlara ulaşamayacağız çünkü. veritabanında toplam 4079 şehir var. Ancak üretim uygulamalarında çok fazla veri almanız gerektiğinde bu teknik sıklıkla kullanılır.

Ve ana yöntemin uygulanması:

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

Artık uygulamamızı ilk defa debug modunda çalıştırabilir ve nasıl çalıştığını (veya çalışmadığını - evet, sıklıkla olur) görebiliriz.

Şehirler oluyor. Daha önce başka bir şehir için veri tabanından çıkarılmamışsa, her şehir bir ülke alır. Hibernate'in veritabanına kaç tane sorgu göndereceğini kabaca hesaplayalım:

  • Toplam şehir sayısını öğrenmek için 1 istek (ne zaman duracağını bilmek için 500'den fazla şehri tekrarlamak gerekiyordu).
  • 4079 / 500 = 9 istek (şehir listesi).
  • Daha önce çıkarılmamışsa, her şehir bir ülke alır. Veritabanında 239 ülke olduğu için bu bize 239 sorgu verecektir.

Total 249 requests. Ayrıca ülke ile birlikte hemen bir dizi dil alacağımızı, aksi takdirde genel olarak karanlık olacağını söyledik. Ama yine de çok fazla, bu yüzden davranışı biraz değiştirelim. Düşüncelerle başlayalım: ne yapmalı, nereye kaçmalı? Ama cidden - neden bu kadar çok istek var? İstek günlüğüne bakarsanız, her ülkenin ayrı ayrı talep edildiğini görüyoruz, bu nedenle ilk basit çözüm: tüm ülkeleri birlikte talep edelim, çünkü bu işlemde hepsine ihtiyacımız olacağını önceden biliyoruz.

FetchData() yönteminde, işlem başladıktan hemen sonra aşağıdaki satırı ekleyin:

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

İstekleri sayıyoruz:

  • 1 - tüm ülkeleri al
  • 239 - başkentinin her ülkesi için sorgu
  • 1 - şehir sayısı talebi
  • 9 - şehir listeleri için istek

Total 250. Fikir güzel ama işe yaramadı. Sorun şu ki, ülkenin başkent (şehir) ile bir bağlantısı var @OneToOne. Ve böyle bir bağlantı varsayılan olarak hemen yüklenir ( FetchType.EAGER). koyalım FetchType.LAZYçünkü neyse daha sonra aynı işlemde tüm şehirleri yükleyeceğiz.

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

Büyük harfler artık ayrı olarak talep edilmiyor, ancak talep sayısı değişmedi. Artık her ülke için ayrı bir sorgu ile CountryLanguage listesi isteniyor . Yani ilerleme var ve doğru yönde ilerliyoruz. Hatırlarsanız dersler, isteğe ek bir birleştirme ekleyerek bir istekte bağımlı verileri olan bir varlık istemek için "join fetch" çözümünü öneriyordu. CountryDAO'da , yöntemdeki HQL sorgusunu şu şekilde yeniden yazıngetAll() :

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

Öğle yemeği. Günlüğe bakıyoruz, istekleri sayıyoruz:

  • 1 - dilleri olan tüm ülkeler
  • 1 - şehir sayısı
  • 9 - şehir listeleri.

Total 11- başardık)) Yalnızca tüm bu metni okumakla kalmayıp, aynı zamanda uygulamayı ayarlamanın her adımından sonra çalıştırmayı denediyseniz, tüm uygulamanın hızlanmasını birkaç kez görsel olarak not etmelisiniz.

Bir veri dönüştürme yöntemi yazın

com.codegym.redis2 sınıf eklediğimiz bir paket oluşturalım : CityCountry (bu şehrin bulunduğu şehir ve ülke verileri) ve Language (dil verileri). Burada, "frenleme isteğinde" genellikle "görev tarafından" talep edilen tüm alanlar bulunmaktadır.

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
}

Ana yöntemde, tüm şehirleri aldıktan sonra satırı ekleyin.

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

Ve bu yöntemi uygulayın:

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

Bu yöntemin kendi kendini açıkladığını düşünüyorum: sadece bir CityCountry varlığı oluşturuyoruz ve onu City , Country , CountryLanguage verileriyle dolduruyoruz .

Redis sunucusunu bir liman konteyneri olarak çalıştırın

Burada 2 seçenek var. İsteğe bağlı "redis-insight'ı kur, Redis'te depolanan verilere bak" adımını yaparsanız, komut tam size göre:

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

Bu adımı atlamaya karar verirseniz, sadece:

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

Aradaki fark, ilk seçenekte, 8001 numaralı bağlantı noktasının, içeride ne depolandığını görmek için harici bir istemciyle bağlanabileceğiniz yerel makineye iletilmesidir. Ve anlamlı isimler verirdim, bu nedenle, redis-stackveya redis.

Başlattıktan sonra, çalışan kapsayıcıların listesini görebilirsiniz. Bunu yapmak için şu komutu çalıştırın:

docker container ls 

Ve bunun gibi bir şey göreceksiniz:

Bir komut bulmanız gerekiyorsa, terminal yardımına (docker help) veya google "how to ..." (örneğin, çalışan container'ı nasıl kaldıracağını docker) arayabilirsiniz.

Ayrıca, ana kurucuda turp istemcisinin başlatılmasını da çağırdık, ancak yöntemin kendisini uygulamadık. Uygulama ekle:

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

başlatma günlüğünde her şeyin yolunda olduğunu ve turp istemcisi aracılığıyla bağlantının hatasız geçtiğini görebilmeniz için eğitim amacıyla sout eklendi.

Redis'e veri yaz

Ana yönteme bir çağrı ekleyin

main.pushToRedis(preparedData); 

Bu yöntem uygulaması ile:

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

    }
}

Burada turp istemcisi ile senkronize bir bağlantı açılır ve sırayla CityCountry tipindeki her nesne turpa yazılır. Turp bir String anahtar-değer deposu olduğundan , anahtar (şehir kimliği) bir dizgeye dönüştürülür. Ve değer aynı zamanda dizgedir, ancak ObjectMapper JSON formatında kullanılır.

Günlükte hata olmadığını çalıştırmak ve kontrol etmek için kalır. Her şey çalıştı.

redis-insight'ı yükleyin, Redis'te depolanan verilere bakın (isteğe bağlı)

Bağlantıdan redis-insight'ı indirin ve kurun. Başladıktan sonra, docker kapsayıcısındaki turp örneğimizi hemen gösterir:

Giriş yaparsanız, tüm anahtarların bir listesini göreceğiz:

Hangi verilerin depolandığını görmek için herhangi bir tuşa gidebilirsiniz:

Redis'ten veri almak için bir yöntem yazın

Test için şu testi kullanıyoruz: 10 CityCountry kaydı alıyoruz. Her biri ayrı bir istekle, ancak tek bir bağlantıda.

Turptan elde edilen veriler, turp müşterimiz aracılığıyla elde edilebilir. Bunu yapmak için id'lerin listesini alan bir metot yazalım.

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

Bence uygulama sezgisel: senkronize bir bağlantı açıyoruz ve her kimlik için, ihtiyacımız olan CityCountry türünün nesnesine dönüştürdüğümüz bir JSON String alıyoruz .

MySQL'den veri almak için bir yöntem yazın

CityDAO sınıfında , getById(Integer id)şehri ülke ile birlikte alacağımız bir yöntem ekleyin :

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

Bir önceki paragrafa benzeterek MySQL için benzer bir metodu Main sınıfına ekleyelim:

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

Özelliklerden, tam nesneyi (proxy taslakları olmadan) aldığınızdan emin olmak için, ülkeden açıkça bir dil listesi talep ediyoruz.

Aynı verileri MySQL ve Redis'ten alma hızını karşılaştırın

Burada hemen ana yöntemin kodunu ve yerel bilgisayarımda elde edilen sonucu vereceğim.

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

Test sırasında bir özellik vardır - MySQL'den gelen veriler yalnızca okunur, bu nedenle uygulamamızın başlatılması arasında yeniden başlatılamaz. Ve Redis'te yazılırlar.

Aynı anahtar için bir kopya eklemeye çalıştığınızda, veriler kolayca güncellenecek olsa da, terminalde uygulama başlatma işlemleri arasında kabı durdurmak docker stop redis-stackve kabı silmek için komutları çalıştırmanızı tavsiye ederim docker rm redis-stack. Bundan sonra kabı turpla tekrar kaldırın docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:latestve ancak bundan sonra uygulamamızı yürütün.

İşte test sonuçlarım:

Toplamda “sık frenleme” talebine verilen yanıtın performansında bir buçuk kat artış sağladık. Ve bu, testte ObjectMapper aracılığıyla en hızlı seri kaldırmayı kullanmadığımız gerçeğini hesaba katıyor . GSON olarak değiştirirseniz, büyük ihtimalle biraz daha zaman "kazanabilirsiniz".

Şu anda, bir programcı ve zaman hakkında bir şaka hatırlıyorum : kodunuzu nasıl yazıp optimize edeceğinizi okuyun ve düşünün.