วันนี้เราจะทำโครงการสุดท้ายในโมดูล JRU ที่สี่ จะเป็นอย่างไร ลองทำงานกับเทคโนโลยีต่างๆ: MySQL, Hibernate, Redis, Docker ตอนนี้หัวข้อเพิ่มเติม
งาน: เรามีฐานข้อมูล MySQL เชิงสัมพันธ์พร้อมสคีมา (ประเทศ-เมือง, ภาษาตามประเทศ) และมีการร้องขอของเมืองบ่อยครั้งซึ่งช้าลง เราพบวิธีแก้ปัญหา - เพื่อย้ายข้อมูลทั้งหมดที่มีการร้องขอบ่อยครั้งไปยัง Redis (ในหน่วยความจำประเภทคีย์-ค่า)
และเราไม่ต้องการข้อมูลทั้งหมดที่จัดเก็บไว้ใน MySQL แต่ต้องการเพียงชุดฟิลด์ที่เลือกเท่านั้น โครงการจะอยู่ในรูปแบบของการสอน นั่นคือที่นี่เราจะแจ้งปัญหาและแก้ไขทันที
เรามาเริ่มกันที่ซอฟต์แวร์ที่เราต้องการ:
- IDEA Ultimate (ใครหมดคีย์ - เขียนถึง Roman ใน Slack)
- Workbench (หรือไคลเอนต์อื่น ๆ สำหรับ MySQL)
- นักเทียบท่า
- Redis-insight - ไม่บังคับ
แผนปฏิบัติการของเรา:
- ตั้งค่านักเทียบท่า (ฉันจะไม่ทำสิ่งนี้ในบทช่วยสอน เพราะแต่ละระบบปฏิบัติการจะมีลักษณะเฉพาะของตัวเอง และมีคำตอบมากมายบนอินเทอร์เน็ตสำหรับคำถาม เช่น "วิธีติดตั้งนักเทียบท่าบน windows") ตรวจสอบว่าทุกอย่างใช้งานได้
- เรียกใช้เซิร์ฟเวอร์ MySQL เป็นคอนเทนเนอร์นักเทียบท่า
- ขยายดัมพ์ _
- สร้างโครงการใน Idea เพิ่มการพึ่งพา Maven
- สร้างโดเมนเลเยอร์
- เขียนเมธอดเพื่อรับข้อมูลทั้งหมดจาก MySQL
- เขียนวิธีการแปลงข้อมูล (ใน Redis เราจะเขียนเฉพาะข้อมูลที่ร้องขอบ่อยๆ)
- เรียกใช้เซิร์ฟเวอร์ Redis เป็นคอนเทนเนอร์นักเทียบท่า
- เขียนข้อมูลไปยัง Redis
- ทางเลือก: ติดตั้ง redis-insight ดูข้อมูลที่จัดเก็บไว้ใน Redis
- เขียนวิธีการรับข้อมูลจาก Redis
- เขียนวิธีการรับข้อมูลจาก MySQL
- เปรียบเทียบความเร็วในการรับข้อมูลเดียวกันจาก 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 ที่มีสามตาราง:
- เมืองเป็นตารางของเมือง
- ประเทศ - ตารางประเทศ
- 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 เป็นไปได้มากว่าคุณจะ "ชนะ" ได้อีกเล็กน้อย
ในขณะนี้ ฉันจำเรื่องตลกเกี่ยวกับโปรแกรมเมอร์และเวลาได้:อ่านและคิดเกี่ยวกับวิธีการเขียนและเพิ่มประสิทธิภาพโค้ดของคุณ
GO TO FULL VERSION