[미션 6-2] DB 이중화 적용

MySQL Replication 개념

강의자료 그대로 옮김

  • MySQL Replication의 master/slave는 1:n 관계입니다.

  • master는 갱신쿼리를 바이너리 로그파일로 기록하고, 이 로그파일의 내용이 slave로 전송되어 순차적으로 실행함으로써 복제됩니다. 따라서 MySQL Replication은 준동시성입니다. I/O 스레드가 비동기로 동작하기에 마스터에서 생성한 바이너리 로그가 슬레이브에 수신되기 전에 장애가 날 경우 손실이 발생할 수 있습니다.

  • 데이터조작쿼리(INSERT, UPDATE, DELETE)는 마스터로, 데이터조회쿼리(SELECT)는 슬레이브로 받아서 부하를 분산할 수 있습니다.

MySQL Replication 실습

mysql 관련 설정들이 궁금해서 공식 문서 를 같이 찾아보았다.

master 서버 설정

$ docker run --name mysql-master -p 13306:3306 -v ~/mysql/master:/etc/mysql/conf.d -e MYSQL_ROOT_PASSWORD=masterpw -d mysql

$ docker exec -it mysql-master /bin/bash
$ mysql -u root -p
mysql> CREATE USER 'fistkim101'@'%' IDENTIFIED WITH mysql_native_password by 'masterpw';
mysql> GRANT REPLICATION SLAVE ON *.* TO 'fistkim101'@'%';

mysql> SHOW MASTER STATUS\G;
*************************** 1. row ***************************
             File: binlog.000002
         Position: 671
     Binlog_Do_DB:
 Binlog_Ignore_DB:
Executed_Gtid_Set:
1 row in set (0.00 sec)

slave 서버 설정

$ docker run --name mysql-slave -p 13307:3306 -v ~/mysql/slave:/etc/mysql/conf.d -e MYSQL_ROOT_PASSWORD=slavepw -d mysql

$ docker exec -it mysql-slave /bin/bash
$ mysql -u root -p

mysql> SET GLOBAL server_id = 2;
mysql> CHANGE MASTER TO MASTER_HOST='172.17.0.1', MASTER_PORT = 13306, MASTER_USER='fistkim101', MASTER_PASSWORD='masterpw', MASTER_LOG_FILE='{master 생성시 로그에 나온 바이너리 파일}', MASTER_LOG_POS={master 생성시 로그에 나온 position};

mysql> START SLAVE;
mysql> SHOW SLAVE STATUS\G;
...
            Slave_IO_Running: Yes
            Slave_SQL_Running: Yes

여기까지만 해줘도 일단 DB 끼리는 master-slave 관계가 형성된다. 그래서 master에 무슨 짓을 하든 똑같이 slave에 반영이 되며, 반대로 slave에서 무슨 짓을 해도 master에는 반영이 안된다. 항상 master -> slave 방향으로 async 하게 replication이 진행된다.

어플리케이션 설정

spring.datasource.hikari.master.username=root
spring.datasource.hikari.master.password=masterpw
spring.datasource.hikari.master.jdbc-url=jdbc:mysql://localhost:13306/subway?useSSL=false&useUnicode=yes&characterEncoding=UTF-8&serverTimezone=UTC&allowPublicKeyRetrieval=true

spring.datasource.hikari.slave.username=root
spring.datasource.hikari.slave.password=slavepw
spring.datasource.hikari.slave.jdbc-url=jdbc:mysql://localhost:13307/subway?useSSL=false&useUnicode=yes&characterEncoding=UTF-8&serverTimezone=UTC&allowPublicKeyRetrieval=true
public class ReplicationRoutingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        boolean isReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
        return (isReadOnly)
                ? Constants.DATASOURCE_KEY_SLAVE
                : Constants.DATASOURCE_KEY_MASTER;
    }
}

여기서 ReplicationRoutingDataSource 를 컴포넌트로 등록해주면 안된다. DataBaseConfig 에서 DataSource를 Bean으로 설정해줄때 사용할 단순한 클래스 자체이기 때문에 이게 Bean으로 등록될 필요가 전혀 없다. 되려, 등록을 할 경우 이게 Bean으로 등록되면서 필수적으로 있어야할 TargetDataSources 가 없어서 에러가 발생한다.

AbstractRoutingDataSource 인터페이스는 다양한 target DataSources 들을 Set에 담아두고 바라보는 key(lookup key)를 상황에 맞게 변경토록 해서 앱에서 사용하는 조건에 따라 다양한 DataSource를 사용할 수 있도록 해주는 추상화 인터페이스이다.

package nextstep.subway.configuration;

import com.zaxxer.hikari.HikariDataSource;
import nextstep.subway.common.Constants;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.sql.DataSource;
import java.util.HashMap;

@Configuration
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
@EnableTransactionManagement
@EnableJpaRepositories(basePackages = {"nextstep.subway"})
class DataBaseConfig {

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

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

    @Bean
    public DataSource routingDataSource(@Qualifier("masterDataSource") DataSource master,
                                        @Qualifier("slaveDataSource") DataSource slave) {
        ReplicationRoutingDataSource routingDataSource = new ReplicationRoutingDataSource();

        HashMap<Object, Object> sources = new HashMap<>();
        sources.put(Constants.DATASOURCE_KEY_MASTER, master);
        sources.put(Constants.DATASOURCE_KEY_SLAVE, slave);

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

        return routingDataSource;
    }

    @Primary
    @Bean
    public DataSource dataSource(@Qualifier("routingDataSource") DataSource routingDataSource) {
        return new LazyConnectionDataSourceProxy(routingDataSource);
    }
}

위와 같이 설정이 끝나면 사용하는 Current Transaction에 따라서 바라보는 DataSource가 달라진다. 아래 예시의 경우 slave DB를 바라본다.

    @Transactional(readOnly = true)
    public List<StationResponse> findAllStations() {
        List<Station> stations = stationRepository.findAll();

        return stations.stream()
                .map(StationResponse::of)
                .collect(Collectors.toList());
    }

Last updated