본문 바로가기
Spring/개념

Spring Boot로 MySQL replication 연동하기

by clearinging 2021. 10. 6.
반응형

Transaction

Spring Transaction 설정 방법

  1. 선언적 방법 : @Transactional를 method나 class에 붙인다
  • 현재 spring 진형에서 선호하는 방식
  1. transaction manager를 통해 생성 : transaction start 를 직접 호출

Transaction 2가지 방식

  • readOnly = true : 읽기 전용이기 때문에 jpa 를 사용할 경우 transaction이 끝나면 마지막에 flush 가 나가지 않습니다.
  • readOnly = false : transaction이 끝나면 마지막에 flush 가 나갑니다

Replication 설정

환경설정

  • 아래 참고 링크에서 mysql을 설정하였습니다
  • mysql master : 3307 port 사용
  • mysql slave : 3308 port 사용

application.yml 설정

spring:
  config:
    activate:
      on-profile: prod
  jpa:
    database: mysql
    hibernate:
      ddl-auto: create-drop
      use-new-id-generator-mappings: false
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQL57Dialect
        dialect.storage_engine: innodb
        default_batch_fetch_size: 500
        format_sql: true
    open-in-view: false
  datasource:
    master:
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://localhost:3307/replication?serverTimezone=UTC
      username: master_usr
      password: root
    slave:
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://localhost:3308/replication?serverTimezone=UTC
      username: slave_usr
      password: root
  • datasource에서 mysql 정보를 넣습니다.
  • default jpa datasource 설정 config에서는 master와 slave라는 정보가 없기 때문에 저희가 이 변수들을 전부 직접 사용해서 설정을 해야합니다.

Routing DataSource 설정

  • master slave를 식별하기 위한 ID값을 부여하기 위해서 data source key라른 클래스를 생성했습니다.
@Component
public class DataSourceKey {
    private static final String MASTER_KEY = "master";
    private static final String SLAVE_KEY = "slave";

    private static final String INDENT = "_";
    private static final int DEFAULT_SLAVE_NUMBER = 1;


    public String getMasterKey() {
        return MASTER_KEY;
    }

    public String getSlaveKeyByNumber(int idx) {
        return SLAVE_KEY + INDENT + idx;
    }

    public String getDefaultSlaveKey() {
        return SLAVE_KEY + INDENT + DEFAULT_SLAVE_NUMBER;
    }
}
  • AbstractRoutingDataSource class는 datasource에서 read 형태와 write/delete형태를 식별해주는 로직 입니다.
  • 그리고 식별된 결과를 반환할 때 Object형태로 식별할 수 있는 고유의 key 값을 반환해야합니다.
  • 현재 예시에서는 DataSourceKey라는 component에서 정의한 String key를 사용하였습니다.
@RequiredArgsConstructor
public class ReplicationRoutingDataSource extends AbstractRoutingDataSource {

    private final DataSourceKey dataSourceKey;

    @Override
    protected Object determineCurrentLookupKey() {
        boolean isReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
        if (isReadOnly) {
            logger.info("Connection Slave");
            return dataSourceKey.getDefaultSlaveKey();
        } else {
            logger.info("Connection Master");
            return dataSourceKey.getMasterKey();
        }
    }
}

ReplicationDataSourceConfig 설정

  1. default data source 설정 config bean 재외
  • 이유: 실제로 저희가 직접 설정하고 싶은 replication된 master slave mysql 설정이 아닌, 기본적으로 application.yml에 작성된 mysql 설정을 사용하는 config bean 이기 때문에 재외 해줘야 합니다.
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
public class ReplicationDataSourceConfig {
}
  1. DataSource에서 설정해야하는 mysql data source 생성 코드 작성
// ... codes
public class ReplicationDataSourceConfig {
    private final DataSourceKey dataSourceKey;

    //.. codes

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create()
                .type(HikariDataSource.class)
                .build();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.slave")
    public DataSource slaveDataSource() {
        return DataSourceBuilder.create()
                .type(HikariDataSource.class)
                .build();
    }
}
  • 위와 같이 application.yml에 설정된 master와 slave에서 작성한 url과 username, password를 받아와 Hikari DataSource에 설정해줍니다
  1. routing data source를 사용해서 master와 slave datasource를 mapping 해주는 기능 추가
    @Bean
    public DataSource routingDataSource(DataSource masterDataSource, DataSource slaveDataSource) {
        ReplicationRoutingDataSource routingDataSource = new ReplicationRoutingDataSource(dataSourceKey);

        Map<Object, Object> sources = Map.of(
                dataSourceKey.getMasterKey(), masterDataSource,
                dataSourceKey.getDefaultSlaveKey(), slaveDataSource
        );

        routingDataSource.setTargetDataSources(sources);
        routingDataSource.setDefaultTargetDataSource(masterDataSource);

        return routingDataSource;
    }
  • 이 설정으로 인해서 Transaction이 readOnly = true일 경우 DataSource가 slave, readOnly = false일 경우 master DB를 가리키는 DataSource가 반환 되도록 하였습니다.
  1. LazyConnectionDataSourceProxy 설정
  • 기존 Connection Pool의 단점
    • Ehcache와 같이 JPA에서 제공하는 2차 캐시를 사용할 경우 실제로 DB에 접근을 하지 않지만, Connection을 잡아 먹는 이슈가 발생합니다
    • Hibernate의 영속성 컨택스트의 값(1차 캐시)을 가져올 때도 Connection이 유지
    • @Transactional이 붙어있는 메서드가 외부 서비스를 호출하고 DB에 정보를 저장하는 경우, 메서드 시작할 때 이미 Connection을 맺기 때문에 외부 서비스 호출 하는 단계에서 부터 Connection이 유지가 됩니다. 결론적으로 불필요한 자원이 할당되게 됩니다
    • Multi Datasource 환경에서 트랜잭션에 진입한 이후 Datasource를 결정하는 경우, 이미 트랜잭션 진입시점에 Datasource가 결정되고 connection이 맺어지게 됩니다. 그러므로 datasource를 선택이 불가능한 이슈가 발생합니다.
  • JpaTransactionManager에서 DB Connection 가져오기
    • doBegin이라는 method에서 datasource의 getConnection이라는 method를 호출해 DB connection을 연결하여 반환합니다
    • HikariDataSource : Transaction이 시작 시점에 바로 getConnection() 메서드가 호출되어 connection pool 연결
    • LazyConnectionDataSourceProxy : getConnection에서는 proxy 객체를 반환, 실제로 Connection이 필요한 시점에만 connection pool을 연결
public interface DataSource  extends CommonDataSource, Wrapper {
    // ... codes

    Connection getConnection() throws SQLException;

    // ... codes
}
  • 결론: master/slave구조와 같이 다중 datasource를 사용할경우 LazyConnectionDataSourceProxy를 사용해서 필요할 때 Datasource를 찾고, connection pool를 점유하는 방식을 사용해야 합니다.
    @Bean
    @Primary
    public DataSource dataSource() {
        return new LazyConnectionDataSourceProxy(routingDataSource(masterDataSource(), slaveDataSource()));
    }
  • 위와 같이 Primary로 등록한 이유는 master/slave datasource도 Bean으로 등록했기 때문에 의존성 이슈가 발생할 수 있어서 설정하였습니다.

TransactionManager자동생성과 Jpa 자동설정 기능 추가

  1. JPA
  • JPA에서 default로 datasource를 설정하는 config파일이 있습니다.
  • 해당 Config설정을 대신해서 앞에서 정의한 Replication Datasource를 사용해야합니다.
  • 일단 JPA default 설정 Config 파일을 재외하기
    @EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
  • DataSource를 Replication으로 위와 같이 설정이 완료되었기 때문에 다음으로 TransactionManager에 data source를 넣는 config를 아래와 같이 호출해야 합니다.
@EnableTransactionManagement

전체 코드

@Configuration
@RequiredArgsConstructor
@EnableTransactionManagement
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
public class ReplicationDataSourceConfig {
    private final DataSourceKey dataSourceKey;

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create()
                .type(HikariDataSource.class)
                .build();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.slave")
    public DataSource slaveDataSource() {
        return DataSourceBuilder.create()
                .type(HikariDataSource.class)
                .build();
    }

    @Bean
    public DataSource routingDataSource(DataSource masterDataSource, DataSource slaveDataSource) {
        ReplicationRoutingDataSource routingDataSource = new ReplicationRoutingDataSource(dataSourceKey);

        Map<Object, Object> sources = Map.of(
                dataSourceKey.getMasterKey(), masterDataSource,
                dataSourceKey.getDefaultSlaveKey(), slaveDataSource
        );

        routingDataSource.setTargetDataSources(sources);
        routingDataSource.setDefaultTargetDataSource(masterDataSource);

        return routingDataSource;
    }

    @Bean
    @Primary
    public DataSource dataSource() {
        return new LazyConnectionDataSourceProxy(routingDataSource(masterDataSource(), slaveDataSource()));
    }
}

참고

docker로 mysql replication 하는 방법

반응형

'Spring > 개념' 카테고리의 다른 글

Servlet과 Spring Container  (1) 2021.12.04
Spring Test Mock 사용법 및 특징  (1) 2021.10.30
04 Exception  (0) 2021.05.19
03 템플릿  (0) 2021.05.08
01 장 오브젝트와 의존관계  (0) 2021.05.05