-
Notifications
You must be signed in to change notification settings - Fork 8
DB Replication Spring Boot 적용
Jinho Huh edited this page Aug 20, 2021
·
26 revisions
-
@Transactional(readOnly=true)
로 지정된 메소드에서 나가는 쿼리는Slave DB
로만 쿼리가 나가게 설정 - 그 외에
readOnly=true
로 지정하지 않은 메소드에서 나가는 쿼리는Master DB
로만 쿼리가 나가게 설정 - 위와 같이 설정하더라도
Transactional의 기존 기능
이 정상적으로 동작 - 위와 같이 설정하더라도
JPA의 기본 동작
에 영향이 없도록 설정 - 추후 고민...
- DB Replication 인프라 구성은 개발 서버의 DB에는 적용하지 않을 예정(Prod, 운영 서버에만 적용)
- 기존 개발서버, 운영서버는 같은 prod properties로 실행하고 있었기 때문에 분리할 필요가 있음
### application-prod.properties
### master db
datasource.url=${DATASOURCE_URL}
datasource.username=${DATASOURCE_USERNAME}
datasource.password=${DATASOURCE_PASSWORD}
### slave-1 db
datasource.slave.slave1.name=slave-1
datasource.slave.slave1.url=${SLAVE1_URL}
### slave-2 db
datasource.slave.slave2.name=slave-2
datasource.slave.slave2.url=${SLAVE2_URL}
### sql 로깅 설정
logging.level.org.hibernate.SQL=debug
logging.level.org.hibernate.type=trace
logging.level.org.hibernate.type.descriptor.sql=trace
### application-dev.properties
datasource.url=${DATASOURCE_URL}
datasource.username=${DATASOURCE_USERNAME}
datasource.password=${DATASOURCE_PASSWORD}
- 위에서 설정한 Master DB와 Slave DB들의 url에 대한 properties에 대한 설정을 객체로 가져오는 설정
- Oauth Login에서 사용한 oauth-properties를 가져오는 걸 생각하면 이해하기 쉬움
@Getter
@Setter
@ConfigurationProperties(prefix = "datasource")
public class CustomDataSourceProperties {
private String url;
private String username;
private String password;
private final Map<String, Slave> slave = new HashMap<>();
@Getter
@Setter
public static class Slave {
private String name;
private String url;
}
}
- Slave List 중에서 Slave의 부하를 분산하기 위해 한 번씩 번갈아가면서 사용하기 위한
CircularList
를 생성
public class CircularList<T> {
private final List<T> list;
private Integer counter = 0;
public CircularList(List<T> list) {
this.list = list;
}
public T getOne() {
if (counter + 1 >= list.size()) {
counter = -1;
}
return list.get(++counter);
}
}
-
readOnly가 true
로 설정되어 있는 Transaction은 slave 중 하나의 datatsource를 사용하도록 지정 - 나머지는 master datasource를 사용
public class ReplicationRoutingDataSource extends AbstractRoutingDataSource {
private CircularList<String> dataSourceNameList;
@Override
public void setTargetDataSources(Map<Object, Object> targetDataSources) {
super.setTargetDataSources(targetDataSources);
dataSourceNameList = new CircularList<>(
targetDataSources.keySet()
.stream()
.filter(key -> key.toString().contains("slave"))
.map(key -> key.toString())
.collect(Collectors.toList())
);
}
@Override
protected Object determineCurrentLookupKey() {
// readOnly가 true인 경우
boolean isReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
if(isReadOnly) {
// slave를 사용하는 경우 확인하기 위한 로깅 설정
logger.info("Connection Slave");
// 위에서 CircularList에서 Slave 중에 하나를 사용
return dataSourceNameList.getOne();
} else {
// master를 사용하는 경우 확인하기 위한 로깅 설정
logger.info("Connection Master");
// master 사용
return "master";
}
}
}
- Spring Boot의
DataSourceAutoConfiguration
을 사용하면 여러 개의 DataSource를 등록할 수 없다. -
DataSourceAutoConfiguration
을 비활성화하는 @Configuration을 추가해야 한다. - 그런데 이
DataSourceAutoConfiguration
을 포기하면 기존의 application.properties를 사용하여 설정하던 Hibernate 설정들이 자동으로 등록되지 않는다. - 결국 직접 DataSourceConfiguration을 직접 작성하여
DataSourceAutoConfiguration 비활성화
하고Hibernate 설정을 직접 주입
해줘야 한다. - 특히 Table, Column에 대한 네이밍 전략까지 직접 지정해줘야 한다.
### application-prod.properties
spring.jpa.properties.hibernate.show_sql=false
spring.jpa.properties.hibernate.generate-ddl=false
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true
# 참고 - https://velog.io/@mumuni/Hibernate5-Naming-Strategy-%EA%B0%84%EB%8B%A8-%EC%A0%95%EB%A6%AC 참고
# 이 설정은 Column과 Table의 명칭을 정하는 기준이 되는 전략에 대한 설정
# SpringPhysicalNamingStrategy - camel case를 underscore 형태로 변경
# PhysicalNamingStrategyStandardImpl - 기본 변수명을 그대로 사용
spring.jpa.properties.hibernate.physical_naming_strategy=org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy
...
### master db
datasource.url=${DATASOURCE_URL}
datasource.username=${DATASOURCE_USERNAME}
datasource.password=${DATASOURCE_PASSWORD}
...
- Spring은 기본적으로 트랜잭션을 시작할 때 쿼리가 실행되기도 전에 DataSource를 정해놓는다. (TransactionManager 식별 -> DataSource에서 Connection 가져오고 -> Transaction 동기화(Synchronization)
- Transaction이 시작되면 같은 DataSource만을 이용한다.
-
우리가 원하는 방식은 쿼리를 실행할 때 DataSource를 정할 수 있도록 미리 DataSource 정하지 않고 늦춰주도록 구현
@Configuration
@Profile("prod")
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
@EnableConfigurationProperties(CustomDataSourceProperties.class)
public class CustomDataSourceConfig {
private final CustomDataSourceProperties databaseProperty;
private final JpaProperties jpaProperties;
public CustomDataSourceConfig(CustomDataSourceProperties databaseProperty, JpaProperties jpaProperties) {
this.databaseProperty = databaseProperty;
this.jpaProperties = jpaProperties;
}
public DataSource createDataSource(String url) {
return DataSourceBuilder.create()
.type(HikariDataSource.class)
.url(url)
.driverClassName("com.mysql.cj.jdbc.Driver")
.username(databaseProperty.getUsername())
.password(databaseProperty.getPassword())
.build();
}
@Bean
public DataSource routingDataSource() {
DataSource master = createDataSource(databaseProperty.getUrl());
Map<Object, Object> dataSourceMap = new LinkedHashMap<>();
dataSourceMap.put("master", master);
databaseProperty.getSlave()
.forEach((key, value) -> dataSourceMap.put(value.getName(), createDataSource(value.getUrl())));
ReplicationRoutingDataSource replicationRoutingDataSource = new ReplicationRoutingDataSource();
replicationRoutingDataSource.setDefaultTargetDataSource(master);
replicationRoutingDataSource.setTargetDataSources(dataSourceMap);
return replicationRoutingDataSource;
}
@Bean
public DataSource dataSource() {
return new LazyConnectionDataSourceProxy(routingDataSource());
}
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
EntityManagerFactoryBuilder entityManagerFactoryBuilder = createEntityManagerFactoryBuilder(jpaProperties);
return entityManagerFactoryBuilder.dataSource(dataSource()).packages("com.wootech.dropthecode").build();
}
private EntityManagerFactoryBuilder createEntityManagerFactoryBuilder(JpaProperties jpaProperties) {
AbstractJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
return new EntityManagerFactoryBuilder(vendorAdapter, jpaProperties.getProperties(), null);
}
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
JpaTransactionManager tm = new JpaTransactionManager();
tm.setEntityManagerFactory(entityManagerFactory);
return tm;
}
}
- TeacherService findAll(readOnly)
- LanguageService -
findAllLanguages(readOnly)
- SkillService -
findAllSkills(readOnly)
- TeacherRepository -
findAllTeacher
- LanguageService -
-
MemberService findMemberByToken(readOnly) ->
Transaction 1
- MemberService -
findById(readOnly)
- MemberService -
-
TeacherService registerTeacher ->
Transaction 2
- MemberService -
findById(readOnly)
- LanguageService -
findAllLanguages(readOnly)
- SkillService -
findAllSkills(readOnly)
- TeacherService -
save
- TeacherLanguageService -
saveAll
- TeacherSkillService -
saveAll
- MemberService -
save
- MemberService -