วันนี้เราจะทำโครงการสุดท้ายในโมดูล JRU ที่สี่ จะเป็นอย่างไร ลองทำงานกับเทคโนโลยีต่างๆ: MySQL, Hibernate, Redis, Docker ตอนนี้หัวข้อเพิ่มเติม

งาน: เรามีฐานข้อมูล MySQL เชิงสัมพันธ์พร้อมสคีมา (ประเทศ-เมือง, ภาษาตามประเทศ) และมีการร้องขอของเมืองบ่อยครั้งซึ่งช้าลง เราพบวิธีแก้ปัญหา - เพื่อย้ายข้อมูลทั้งหมดที่มีการร้องขอบ่อยครั้งไปยัง Redis (ในหน่วยความจำประเภทคีย์-ค่า)

และเราไม่ต้องการข้อมูลทั้งหมดที่จัดเก็บไว้ใน MySQL แต่ต้องการเพียงชุดฟิลด์ที่เลือกเท่านั้น โครงการจะอยู่ในรูปแบบของการสอน นั่นคือที่นี่เราจะแจ้งปัญหาและแก้ไขทันที

เรามาเริ่มกันที่ซอฟต์แวร์ที่เราต้องการ:

  1. IDEA Ultimate (ใครหมดคีย์ - เขียนถึง Roman ใน Slack)
  2. Workbench (หรือไคลเอนต์อื่น ๆ สำหรับ MySQL)
  3. นักเทียบท่า
  4. Redis-insight - ไม่บังคับ

แผนปฏิบัติการของเรา:

  1. ตั้งค่านักเทียบท่า (ฉันจะไม่ทำสิ่งนี้ในบทช่วยสอน เพราะแต่ละระบบปฏิบัติการจะมีลักษณะเฉพาะของตัวเอง และมีคำตอบมากมายบนอินเทอร์เน็ตสำหรับคำถาม เช่น "วิธีติดตั้งนักเทียบท่าบน windows") ตรวจสอบว่าทุกอย่างใช้งานได้
  2. เรียกใช้เซิร์ฟเวอร์ MySQL เป็นคอนเทนเนอร์นักเทียบท่า
  3. ขยายดัมพ์ _
  4. สร้างโครงการใน Idea เพิ่มการพึ่งพา Maven
  5. สร้างโดเมนเลเยอร์
  6. เขียนเมธอดเพื่อรับข้อมูลทั้งหมดจาก MySQL
  7. เขียนวิธีการแปลงข้อมูล (ใน Redis เราจะเขียนเฉพาะข้อมูลที่ร้องขอบ่อยๆ)
  8. เรียกใช้เซิร์ฟเวอร์ Redis เป็นคอนเทนเนอร์นักเทียบท่า
  9. เขียนข้อมูลไปยัง Redis
  10. ทางเลือก: ติดตั้ง redis-insight ดูข้อมูลที่จัดเก็บไว้ใน Redis
  11. เขียนวิธีการรับข้อมูลจาก Redis
  12. เขียนวิธีการรับข้อมูลจาก MySQL
  13. เปรียบเทียบความเร็วในการรับข้อมูลเดียวกันจาก MySQL และ Redis

การตั้งค่านักเทียบท่า

Docker เป็นแพลตฟอร์มแบบเปิดสำหรับการพัฒนา ส่งมอบ และใช้งานแอปพลิเคชัน เราจะใช้เพื่อไม่ให้ติดตั้งและกำหนดค่า Redis บนเครื่องโลคัล แต่เพื่อใช้อิมเมจสำเร็จรูป คุณสามารถอ่านเพิ่มเติมเกี่ยวกับนักเทียบท่าได้ที่นี่หรือดูที่นี่ หากคุณไม่คุ้นเคยกับนักเทียบท่า ฉันขอแนะนำให้ดูที่ลิงก์ที่สอง

เพื่อให้แน่ใจว่าคุณได้ติดตั้งและกำหนดค่านักเทียบท่าแล้ว ให้รันคำสั่ง:docker -v

หากทุกอย่างเรียบร้อยดี คุณจะเห็นเวอร์ชันนักเทียบท่า

เรียกใช้เซิร์ฟเวอร์ MySQL เป็นคอนเทนเนอร์นักเทียบท่า

เพื่อให้สามารถเปรียบเทียบเวลาในการส่งคืนข้อมูลจาก MySQL และ Redis เราจะใช้ MySQL ใน docker ด้วย ใน 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 พร้อมรูทค่าไปยังคอนเทนเนอร์ ตั้งค่าสถานะเฉพาะสำหรับ mysql/ รูปภาพ
  • --restart unless-stopped- การตั้งค่านโยบายพฤติกรรม (ว่าควรรีสตาร์ทคอนเทนเนอร์เมื่อปิดหรือไม่) ค่า unless-stop หมายถึงการเริ่มใหม่เสมอ ยกเว้นเมื่อคอนเทนเนอร์หยุดทำงาน /
  • -v mysql:/var/lib/mysql – เพิ่มปริมาณ (ภาพสำหรับเก็บข้อมูล)
  • mysql:8 – ชื่อภาพและเวอร์ชั่น

หลังจากรันคำสั่งในเทอร์มินัล นักเทียบท่าจะดาวน์โหลดเลเยอร์ทั้งหมดของอิมเมจและเริ่มคอนเทนเนอร์:

หมายเหตุสำคัญ:หากคุณติดตั้ง MySQL เป็นบริการบนเครื่องคอมพิวเตอร์ของคุณ และกำลังทำงานอยู่ คุณต้องระบุพอร์ตอื่นในคำสั่ง start หรือหยุดบริการที่กำลังทำงานอยู่นี้

ขยายการถ่ายโอนข้อมูล

หากต้องการขยายดัมพ์ คุณต้องสร้างการเชื่อมต่อใหม่ไปยังฐานข้อมูลจาก Workbench ซึ่งคุณระบุพารามิเตอร์ ฉันใช้พอร์ตเริ่มต้น (3306) ไม่ได้เปลี่ยนชื่อผู้ใช้ (รูทตามค่าเริ่มต้น) และตั้งรหัสผ่านสำหรับผู้ใช้รูท (รูท)

ใน Workbench ให้ทำData Import/RestoreและImport from Self Contained Fileเลือก ระบุตำแหน่ง ที่ คุณดาวน์โหลด ดัมพ์เป็นไฟล์ คุณไม่จำเป็นต้องสร้างสคีมาล่วงหน้า - การสร้างสคีมาจะรวมอยู่ในไฟล์ดัมพ์ หลังจากนำเข้าสำเร็จ คุณจะมี world schema ที่มีสามตาราง:

  1. เมืองเป็นตารางของเมือง
  2. ประเทศ - ตารางประเทศ
  3. country_language - ตารางที่ระบุเปอร์เซ็นต์ของประชากรในประเทศที่พูดภาษาใดภาษาหนึ่ง

เนื่องจากเราใช้โวลุ่มเมื่อเริ่มต้นคอนเทนเนอร์ หลังจากหยุดและแม้กระทั่งลบคอนเทนเนอร์ mysql และเรียกใช้คำสั่ง start ( ) ใหม่อีกครั้ง จึงไม่มีความจำเป็นต้อง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> 

การพึ่งพาสามรายการแรกนั้นคุ้นเคยกับคุณมานานแล้ว

lettuce-coreเป็นหนึ่งในไคลเอนต์ Java ที่มีอยู่สำหรับการทำงานกับ Redis

jackson-databind– การพึ่งพาการใช้ ObjectMapper (เพื่อแปลงข้อมูลสำหรับการจัดเก็บใน Redis (คีย์-ค่าของประเภท String))

นอกจากนี้ในโฟลเดอร์ทรัพยากร (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 จุดที่น่าสนใจในโค้ด

อย่างแรกคือ Continent enamซึ่งเก็บไว้ในฐานข้อมูลเป็นค่าลำดับ ในโครงสร้างของตารางประเทศ ในความคิดเห็นเกี่ยวกับฟิลด์ทวีป คุณสามารถดูได้ว่าค่าตัวเลขใดสอดคล้องกับทวีปใด

package com.codegym.domain;

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

จุดที่สองคือชุดของเอนทิCountryLanguageตี นี่คือลิงก์@OneToManyที่ไม่ได้อยู่ในร่างที่สองของโมดูลนี้ ตามค่าเริ่มต้น ไฮเบอร์เนตจะไม่ดึงค่าของชุดนี้เมื่อขอเอนทิตีประเทศ แต่เนื่องจากเราจำเป็นต้องลบค่าทั้งหมดออกจากฐานข้อมูลเชิงสัมพันธ์สำหรับการแคชFetchType.EAGER.

ที่สามคือสนามเมือง การสื่อสาร@OneToOne- เหมือนทุกอย่างคุ้นเคยและเข้าใจได้ แต่ถ้าเราดูโครงสร้างคีย์ต่างประเทศในฐานข้อมูล เราจะเห็นว่าประเทศ (ประเทศ) มีการเชื่อมโยงไปยังเมืองหลวง (เมือง) และเมือง (เมือง) มีการเชื่อมโยงไปยังประเทศ (ประเทศ) มีความสัมพันธ์เป็นวัฏจักร

เราจะไม่ทำอะไรกับสิ่งนี้แต่เมื่อเราไปที่รายการ “Write a method for all data from 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

}

รหัสชั้นเรียนประเทศภาษา:

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

ในคลาสหลัก เราประกาศฟิลด์:

private final SessionFactory sessionFactory;
private final RedisClient redisClient;

private final ObjectMapper mapper;

private final CityDAO cityDAO;
private final CountryDAO countryDAO;

และเริ่มต้นในคอนสตรัคเตอร์ของคลาสหลัก:

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 ยังขาดสองวิธี:

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 เมืองในฐานข้อมูล แต่ในการใช้งานจริง เมื่อคุณต้องการข้อมูลจำนวนมาก มักจะใช้เทคนิคนี้

และการดำเนินการตามวิธีการหลัก:

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 requests. และเรายังกล่าวอีกว่าเมื่อรวมกับประเทศแล้วเราจะได้รับชุดภาษาทันที มิฉะนั้นจะมีความมืดมนโดยทั่วไป แต่ยังเยอะอยู่ เลยปรับพฤติกรรมกันสักหน่อย เริ่มจากความคิด: จะทำอะไร วิ่งที่ไหน? แต่อย่างจริงจัง - ทำไมมีคำขอมากมาย หากคุณดูที่บันทึกคำขอ เราจะเห็นว่ามีการขอแต่ละประเทศแยกกัน ดังนั้นวิธีแก้ปัญหาง่ายๆ ข้อแรก: ลองขอทุกประเทศพร้อมกัน เพราะเราทราบล่วงหน้าว่าเราต้องการทุกประเทศในการทำธุรกรรมนี้

ในเมธอด 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 จะถูกร้องขอโดยข้อความค้นหาแยกต่างหาก นั่นคือมีความคืบหน้าและเรากำลังเดินไปในทิศทางที่ถูกต้อง หากคุณจำได้ การบรรยายได้แนะนำ วิธีแก้ปัญหา "join fetch"เพื่อขอเอนทิตีที่มีข้อมูลอ้างอิงในคำขอเดียวโดยเพิ่มการรวมเพิ่มเติมในคำขอ ในCountryDAOให้เขียนคิวรี HQL ใหม่ในเมธอดgetAll()เพื่อ:

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

ปล่อย. เราดูบันทึก นับคำขอ:

  • 1 - ทุกประเทศที่มีภาษา
  • 1 - จำนวนเมือง
  • 9 - รายชื่อเมือง

Total 11- เราประสบความสำเร็จ)) หากคุณไม่เพียง แต่อ่านข้อความทั้งหมดนี้ แต่ยังพยายามเรียกใช้หลังจากแต่ละขั้นตอนของการปรับแต่งแอปพลิเคชันคุณควรสังเกตการเร่งความเร็วของแอปพลิเคชันทั้งหมดด้วยสายตาหลาย ๆ ครั้ง

เขียนวิธีการแปลงข้อมูล

มาสร้างแพ็คเกจcom.codegym.redisที่เราเพิ่ม 2 คลาส: CityCountry (ข้อมูลเกี่ยวกับเมืองและประเทศที่เมืองนี้ตั้งอยู่) และLanguage (ข้อมูลเกี่ยวกับภาษา) นี่คือฟิลด์ทั้งหมดที่มักถูกร้องขอ "ตามงาน" ใน "คำขอเบรก"

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
}

ในเมธอดหลัก หลังจากได้เมืองทั้งหมดแล้ว ให้เพิ่มบรรทัด

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

ฉันคิดว่าวิธีนี้อธิบายได้ในตัว: เราเพิ่งสร้าง เอน ทิ ตี CityCountryและกรอกข้อมูลจากCity , Country , CountryLanguage

เรียกใช้เซิร์ฟเวอร์ Redis เป็นคอนเทนเนอร์นักเทียบท่า

มี 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-stackหรือredis.

หลังจากเปิดตัว คุณจะเห็นรายการคอนเทนเนอร์ที่กำลังทำงานอยู่ ในการทำเช่นนี้ ให้รันคำสั่ง:

docker container ls 

และคุณจะเห็นสิ่งนี้:

หากคุณต้องการค้นหาคำสั่งบางอย่าง คุณสามารถดูวิธีใช้ในเทอร์มินัล (วิธีใช้นักเทียบท่า) หรือ google "how to ..." (เช่น นักเทียบท่าวิธีลบคอนเทนเนอร์ที่กำลังทำงานอยู่)

และเรายังเรียกการเริ่มต้นของไคลเอนต์หัวไชเท้าในตัวสร้างหลัก แต่ไม่ได้ใช้วิธีการเอง เพิ่มการใช้งาน:

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

    }
}

ที่นี่ การเชื่อมต่อแบบซิงโครนัสจะเปิดขึ้นกับไคลเอ็นต์หัวไชเท้า และแต่ละออบเจ็กต์ของ ประเภท CityCountryจะถูกเขียนไปยังหัวไชเท้า ตามลำดับ เนื่องจากหัวไชเท้าเป็น ที่เก็บคีย์-ค่าของ สตริงคีย์ (รหัสเมือง) จึงถูกแปลงเป็นสตริง และค่ายังเป็นสตริง แต่ใช้ObjectMapperในรูปแบบ JSON

ยังคงทำงานและตรวจสอบว่าไม่มีข้อผิดพลาดในบันทึก ทุกอย่างทำงาน

ติดตั้ง Redis-Insight ดูข้อมูลที่จัดเก็บไว้ใน Redis (ไม่บังคับ)

ดาวน์โหลด redis-insight จากลิงค์ และติดตั้ง หลังจากเริ่มต้น มันจะแสดงอินสแตนซ์หัวไชเท้าของเราในคอนเทนเนอร์นักเทียบท่าทันที:

หากคุณเข้าสู่ระบบ เราจะเห็นรายการคีย์ทั้งหมด:

และคุณสามารถไปที่คีย์ใดก็ได้เพื่อดูว่าเก็บข้อมูลอะไรไว้บ้าง:

เขียนวิธีการรับข้อมูลจาก Redis

สำหรับการทดสอบ เราใช้การทดสอบต่อไปนี้: เราได้รับบันทึก CityCountry 10 รายการ แต่ละรายการมีคำขอแยกกัน แต่เชื่อมต่อกัน

ข้อมูลจากหัวไชเท้าสามารถรับได้จากไคลเอนต์หัวไชเท้าของเรา ในการทำเช่นนี้ ลองเขียนเมธอดที่รับรายการ 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 ซึ่งเราแปลงเป็นวัตถุประเภท CityCountryที่เราต้องการ

เขียนเมธอดเพื่อรับข้อมูลจาก 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 ให้กับคลาสหลัก:

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และหลังจากนั้นให้ดำเนินการแอปพลิเคชันของเราเท่านั้น

นี่คือผลการทดสอบของฉัน:

โดยรวมแล้ว เราประสบความสำเร็จในการเพิ่มประสิทธิภาพการตอบสนองต่อคำขอ "เบรกบ่อย" หนึ่งเท่าครึ่ง และนี่คือการพิจารณาความจริงที่ว่าในการทดสอบเราไม่ได้ใช้การดีซีเรียลไลเซชันที่เร็วที่สุดผ่านObjectMapper หากคุณเปลี่ยนเป็น GSON เป็นไปได้มากว่าคุณจะ "ชนะ" ได้อีกเล็กน้อย

ในขณะนี้ ฉันจำเรื่องตลกเกี่ยวกับโปรแกรมเมอร์และเวลาได้:อ่านและคิดเกี่ยวกับวิธีการเขียนและเพิ่มประสิทธิภาพโค้ดของคุณ