오늘 우리는 네 번째 JRU 모듈에 대한 최종 프로젝트를 수행할 것입니다. 그것은 무엇입니까? MySQL, Hibernate, Redis, Docker와 같은 다양한 기술로 작업해 봅시다. 이제 더 많은 주제.
작업: 스키마(국가-도시, 국가별 언어)가 있는 관계형 MySQL 데이터베이스가 있습니다. 그리고 속도를 늦추는 도시의 빈번한 요청이 있습니다. 자주 요청하는 모든 데이터를 Redis(키-값 유형의 메모리 저장소)로 옮기는 솔루션을 찾았습니다.
그리고 MySQL에 저장된 모든 데이터가 필요하지 않고 선택한 필드 집합만 필요합니다. 프로젝트는 튜토리얼 형식으로 진행됩니다. 즉, 여기서 문제를 제기하고 즉시 해결할 것입니다.
이제 필요한 소프트웨어부터 시작하겠습니다.
- IDEA Ultimate(열쇠가 부족한 사람 - Slack에서 Roman에게 쓰기)
- Workbench(또는 MySQL용 기타 클라이언트)
- 도커
- redis-insight - 옵션
우리의 실행 계획:
- 도커를 설정합니다(각 OS마다 고유한 특성이 있고 "Windows에 도커를 설치하는 방법"과 같은 질문에 대한 답변이 인터넷에 많이 있기 때문에 튜토리얼에서는 이 작업을 수행하지 않습니다) 모든 것이 작동하는지 확인합니다.
- MySQL 서버를 도커 컨테이너로 실행합니다.
- 덤프를 확장하십시오 .
- Idea에서 프로젝트를 생성하고 maven 종속성을 추가합니다.
- 레이어 도메인을 만듭니다.
- MySQL에서 모든 데이터를 가져오는 메서드를 작성합니다.
- 데이터 변환 방법을 작성합니다(Redis에서는 자주 요청되는 데이터만 작성합니다).
- Redis 서버를 도커 컨테이너로 실행합니다.
- Redis에 데이터를 씁니다.
- 옵션: redis-insight를 설치하고 Redis에 저장된 데이터를 확인합니다.
- Redis에서 데이터를 가져오는 방법을 작성합니다.
- MySQL에서 데이터를 가져오는 방법을 작성하십시오.
- MySQL과 Redis에서 동일한 데이터를 가져오는 속도를 비교하십시오.
도커 설정
Docker는 애플리케이션을 개발, 제공 및 운영하기 위한 개방형 플랫폼입니다. 로컬 머신에 Redis를 설치 및 구성하는 것이 아니라 기성품 이미지를 사용하기 위해 사용합니다. 여기에서 docker에 대한 자세한 내용을 읽거나 여기에서 확인할 수 있습니다 . docker에 익숙하지 않은 경우 두 번째 링크만 살펴보는 것이 좋습니다.
docker가 설치 및 구성되었는지 확인하려면 다음 명령을 실행합니다.docker -v
모든 것이 정상이면 도커 버전이 표시됩니다.
![](https://cdn.codegym.cc/images/article/1bc6d1a9-3231-461f-a31c-46ce4a487c04/512.jpeg)
도커 컨테이너로 MySQL 서버 실행
MySQL과 Redis에서 데이터를 반환하는 시간을 비교할 수 있도록 도커에서도 MySQL을 사용합니다. 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
- 동작 정책 설정(컨테이너가 닫힐 때 다시 시작해야 하는지 여부). [unstopped] 값은 컨테이너가 중지된 경우를 제외하고 항상 다시 시작하는 것을 의미합니다.-v mysql:/var/lib/mysql
– 볼륨을 추가합니다(정보 저장용 이미지).mysql:8
– 이미지 이름 및 해당 버전.
터미널에서 명령을 실행한 후 도커는 이미지의 모든 레이어를 다운로드하고 컨테이너를 시작합니다.
![](https://cdn.codegym.cc/images/article/2d4880df-a10e-4553-9354-b854018f53c7/1024.jpeg)
중요 사항: MySQL이 로컬 컴퓨터에 서비스로 설치되어 있고 실행 중인 경우 시작 명령에서 다른 포트를 지정하거나 실행 중인 이 서비스를 중지해야 합니다.
![](https://cdn.codegym.cc/images/article/5354d6b9-187d-4514-b504-e157b1ef003a/1024.jpeg)
덤프 확장
덤프를 확장하려면 매개 변수를 지정하는 Workbench에서 데이터베이스에 대한 새 연결을 생성해야 합니다. 나는 기본 포트(3306)를 사용했고, 사용자 이름(기본적으로 루트)을 변경하지 않았으며, 루트 사용자(루트)의 암호를 설정했습니다.
![](https://cdn.codegym.cc/images/article/7e1b4972-af30-4aa7-9212-c99ff69920f2/1024.jpeg)
Workbench에서 수행 Data Import/Restore
하고 를 선택합니다 Import from Self Contained File
. 덤프를 파일로 다운로드한 위치를 지정합니다 . 미리 스키마를 생성할 필요가 없습니다. 스키마 생성은 덤프 파일에 포함됩니다. 가져오기에 성공하면 세 개의 테이블이 있는 세계 스키마가 생깁니다.
- city는 도시의 테이블입니다.
- 국가 - 국가 테이블.
- country_language - 해당 국가의 인구 중 특정 언어를 사용하는 비율을 나타내는 테이블입니다.
![](https://cdn.codegym.cc/images/article/697339c6-8214-483a-9283-6696ad2ecf7d/512.jpeg)
컨테이너를 시작할 때 볼륨을 사용했기 때문에 mysql 컨테이너를 중지 및 삭제하고 시작 명령( 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 의존성 추가
아이디어에서 프로젝트를 만드는 방법을 이미 알고 있습니다. 이것이 오늘 프로젝트에서 가장 쉬운 지점입니다.
![](https://cdn.codegym.cc/images/article/db14c218-aea0-4108-9a23-0ce172271cf0/1024.jpeg)
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
Redis 작업에 사용할 수 있는 Java 클라이언트 중 하나입니다.
jackson-databind
– ObjectMapper 사용에 대한 종속성(Redis(문자열 유형의 키-값)에 저장하기 위해 데이터 변환).
또한 자원 폴더(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 패키지 생성
Idea에서 테이블 구조를 사용하도록 엔터티의 테이블을 매핑할 때 편리하므로 Idea에서 데이터베이스 연결을 추가해 보겠습니다.
![](https://cdn.codegym.cc/images/article/7c7320ea-4dd5-4eff-b757-503a401565fb/800.jpeg)
![](https://cdn.codegym.cc/images/article/91b9ed51-e724-4cb9-900c-56e06d499c43/1024.jpeg)
다음 순서로 엔터티를 만드는 것이 좋습니다.
- 국가
- 도시
- 국가 언어
매핑은 사용자가 직접 수행하는 것이 바람직합니다.
국가 클래스 코드:
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
다음은 이 모듈의 두 번째 초안에는 없었던 링크입니다 . 기본적으로 Hibernate는 국가 엔터티를 요청할 때 이 세트의 값을 가져오지 않습니다. 하지만 캐싱을 위해 관계형 데이터베이스에서 모든 값을 빼야 하므로 FetchType.EAGER
.
세 번째는 도시 필드입니다 . 의사 소통 @OneToOne
- 모든 것이 친숙하고 이해할 수 있습니다. 그러나 데이터베이스의 외래 키 구조를 보면 국가(country)는 수도(city)에 대한 링크가 있고 도시(city)는 국가(country)에 대한 링크가 있음을 알 수 있습니다. 순환 관계가 있습니다.
우리는 아직 이것으로 아무것도 하지 않을 것이지만, “Write a method for get 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
}
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
}
MySQL에서 모든 데이터를 가져오는 메서드 작성
Main 클래스에서 필드를 선언합니다.
private final SessionFactory sessionFactory;
private final RedisClient redisClient;
private final ObjectMapper mapper;
private final CityDAO cityDAO;
private final CountryDAO countryDAO;
Main 클래스의 생성자에서 초기화합니다.
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());
}
}
이제 이 두 클래스를 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;
}
우리는 아직 radish에 도달하지 않았으므로 radish 클라이언트의 초기화 구현은 당분간 스텁으로 남을 것입니다.
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();
}
이제 처음으로 디버그에서 응용 프로그램을 실행하고 어떻게 작동하는지 확인할 수 있습니다(또는 작동하지 않음 - 예, 종종 발생합니다).
![](https://cdn.codegym.cc/images/article/016f2ea1-822d-43eb-a572-39286c2c4830/800.jpeg)
도시가 점점 늘어나고 있습니다. 이전에 다른 도시의 데이터베이스에서 빼지 않은 경우 각 도시는 국가를 가져옵니다. 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 목록이 별도의 쿼리로 요청됩니다 . 즉, 진전이 있고 올바른 방향으로 나아가고 있습니다. 요청에 추가 조인을 추가하여 하나의 요청에 종속 데이터가 있는 엔터티를 요청하기 위해 강의에서 "조인 가져오기" 솔루션을 제안한 것을 기억하십니까? CountryDAO 에서 메서드의 HQL 쿼리를 getAll()
다음과 같이 다시 작성합니다.
"select c from Country c join fetch c.languages"
시작하다. 로그를 보고 요청을 계산합니다.
- 1 - 언어가 있는 모든 국가
- 1 - 도시 수
- 9 - 도시 목록.
Total 11
- 우리는 성공했습니다)) 이 텍스트를 모두 읽을 뿐만 아니라 응용 프로그램을 조정하는 각 단계 후에 실행을 시도했다면 전체 응용 프로그램의 가속을 여러 번 시각적으로 기록해야 합니다.
데이터 변환 방법 작성
CityCountry (이 도시가 위치한 도시 및 국가에 대한 데이터) 및 Language (언어에 대한 데이터) com.codegym.redis
의 두 클래스를 추가하는 패키지를 만들어 봅시다 . 다음은 "제동 요청"에서 "작업별"로 자주 요청되는 모든 필드입니다.
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
그러면 다음과 같은 내용이 표시됩니다.
![](https://cdn.codegym.cc/images/article/5c44653a-fa4e-4e4c-ac41-c4e5ed26ebf6/1024.jpeg)
명령을 찾아야 하는 경우 터미널 도움말(docker 도움말) 또는 Google "how to ..."(예: docker how to remove running container)를 볼 수 있습니다.
그리고 Main 생성자에서 무 클라이언트의 초기화도 호출했지만 메서드 자체는 구현하지 않았습니다. 구현 추가:
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 유형의 각 개체가 무에 기록됩니다. 무는 문자열 키 -값 저장소 이므로 키(도시 ID)가 문자열로 변환됩니다. 그리고 값도 문자열이지만 JSON 형식의 ObjectMapper를 사용합니다 .
로그에 오류가 없는지 실행하고 확인해야 합니다. 모든 것이 작동했습니다.
redis-insight 설치, Redis에 저장된 데이터 확인(선택사항)
링크 에서 redis-insight를 다운로드하여 설치합니다. 시작하면 즉시 도커 컨테이너에 무 인스턴스가 표시됩니다.
![](https://cdn.codegym.cc/images/article/15311a8f-a8af-4a6d-862b-cb10ca70d2ef/800.jpeg)
로그인하면 모든 키 목록이 표시됩니다.
![](https://cdn.codegym.cc/images/article/97477080-6693-421a-81da-742753a3de5e/800.jpeg)
그리고 아무 키로 이동하여 어떤 데이터가 저장되어 있는지 확인할 수 있습니다.
![](https://cdn.codegym.cc/images/article/7f0836ac-09ba-4c0a-a759-2f85b4c29947/800.jpeg)
Redis에서 데이터를 가져오는 방법 작성
테스트를 위해 다음 테스트를 사용합니다. 10개의 CityCountry 레코드를 얻습니다. 각각 별도의 요청이 있지만 하나의 연결에 있습니다.
무의 데이터는 무 클라이언트를 통해 얻을 수 있습니다. 이를 위해 가져올 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에 대한 유사한 메서드를 Main 클래스에 추가해 보겠습니다.
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
그 후에야 응용 프로그램을 실행하십시오.
내 테스트 결과는 다음과 같습니다.
![](https://cdn.codegym.cc/images/article/51a91b8a-d86c-433b-ab4a-085c73a0c814/800.jpeg)
전체적으로 우리는 "빈번한 제동" 요청에 대한 응답 성능을 1.5배 증가시켰습니다. 그리고 이것은 테스트에서 우리가 ObjectMapper 를 통한 가장 빠른 직렬화 해제를 사용하지 않았다는 사실을 고려한 것입니다 . GSON으로 변경하면 아마도 조금 더 시간을 "승리"할 수 있습니다.
이 순간, 나는 프로그래머와 시간에 대한 농담을 기억합니다. 코드를 작성하고 최적화하는 방법에 대해 읽고 생각하십시오.