반응형

들어가며

설명

Application

// DataSource를 직접 설정하는 방식이므로 자동 설정 클래스를 제외 처리
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class ReplicaExampleApplication { ... }

application.properties

...
replica-datasource.master.url=jdbc:mysql://localhost:13306/test
replica-datasource.master.username=im_master
replica-datasource.master.password=123456
replica-datasource.master.driver-class-name=com.mysql.cj.jdbc.Driver

replica-datasource.slaves[0].url=jdbc:mysql://localhost:23306/test
replica-datasource.slaves[0].username=im_slave
replica-datasource.slaves[0].password=123456
replica-datasource.slaves[0].driver-class-name=com.mysql.cj.jdbc.Driver

replica-datasource.slaves[1].url=jdbc:mysql://localhost:33306/test
replica-datasource.slaves[1].username=im_slave
replica-datasource.slaves[1].password=123456
replica-datasource.slaves[1].driver-class-name=com.mysql.cj.jdbc.Driver

ReplicaDataSourceProperties

@Data
@Component
@ConfigurationProperties("replica-datasource")
public class ReplicaDataSourceProperties {
    private DataSourceProperty master;
    private List<DataSourceProperty> slaves;

    @Data
    public static class DataSourceProperty {
        private String url;
        private String username;
        private String password;
        private String driverClassName;
    }
}

ReplicaDBConfig

@Configuration
public class ReplicaDBConfig {
    @Bean
    public DataSource routingDataSource(ReplicaDataSourceProperties replicaDataSourceProperties) {
        Map<Object, Object> dataSources = createDataSources(replicaDataSourceProperties);
        ReplicaRoutingDataSource replicaRoutingDataSource = new ReplicaRoutingDataSource(dataSources.size() - 1);
        replicaRoutingDataSource.setTargetDataSources(dataSources);
        replicaRoutingDataSource.setDefaultTargetDataSource(dataSources.get("master"));
        return replicaRoutingDataSource;
    }

    @Bean
    public DataSource dataSource(DataSource routingDataSource) {
        // 트랜잭션 실행시에 Connection 객체를 가져오기 위해 LazyConnectionDataSourceProxy로 설정
        return new LazyConnectionDataSourceProxy(routingDataSource);
    }

    private Map<Object, Object> createDataSources(ReplicaDataSourceProperties replicaDataSourceProperties) {
        Map<Object, Object> dataSources = new LinkedHashMap<>();
        dataSources.put("master", createDataSource(replicaDataSourceProperties.getMaster()));

        IntStream.range(0, replicaDataSourceProperties.getSlaves().size()).forEach(i -> {
            dataSources.put("slave-" + i, createDataSource(replicaDataSourceProperties.getSlaves().get(i)));
        });

        return dataSources;
    }

    private DataSource createDataSource(ReplicaDataSourceProperties.DataSourceProperty dataSourceProperty) {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(dataSourceProperty.getUrl());
        dataSource.setDriverClassName(dataSourceProperty.getDriverClassName());
        dataSource.setUsername(dataSourceProperty.getUsername());
        dataSource.setPassword(dataSourceProperty.getPassword());
        return dataSource;
    }

    @Slf4j
    @RequiredArgsConstructor
    private static class ReplicaRoutingDataSource extends AbstractRoutingDataSource {
        private final int slaveSize;

        @Override
        protected Object determineCurrentLookupKey() {
            String dataSourceName = TransactionSynchronizationManager.isCurrentTransactionReadOnly()
                ? "slave-" + hash()
                : "master";

            log.info("[DATA_SOURCE_NAME] : {}", dataSourceName);

            return dataSourceName;
        }

        private int hash() {
            return Math.abs(new Random().nextInt()) % slaveSize;
        }
    }
}

JpaConfig

@Configuration
public class JpaConfig {
    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean entityManagerFactory = new LocalContainerEntityManagerFactoryBean();
        entityManagerFactory.setDataSource(dataSource);
        entityManagerFactory.setPackagesToScan("com.example.replica");
        entityManagerFactory.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
        entityManagerFactory.setJpaPropertyMap(Map.of(
            "hibernate.show_sql", true,
            "hibernate.format_sql", true,
            "hibernate.use_sql_comments", false,
            "hibernate.dialect", "org.hibernate.dialect.MySQL5InnoDBDialect"
        ));
        return entityManagerFactory;
    }

    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        return new JpaTransactionManager(entityManagerFactory);
    }
}

User

@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class User {
    @Id
    private String userId;
    private String name;
}

UserRepository

@Repository
public interface UserRepository extends JpaRepository<User, String> {
}

Test

@Rollback(false)
@SpringBootTest
public class ReplicaTest {
    @Autowired
    private UserRepository userRepository;

    @Transactional // master 접속
    @Test
    public void save() {
        userRepository.save(new User("1", "john"));
        userRepository.save(new User("2", "jane"));
    }
    
    @Transactional(readOnly = true) // slave-x 접속
    @Test
    public void findById() {
        User user1 = userRepository.findById("1").get();
        User user2 = userRepository.findById("2").get();

        assertEquals("john", user1.getName());
        assertEquals("jane", user2.getName());
    }
}

참고

반응형

'Development > Spring' 카테고리의 다른 글

[Spring] actuator  (0) 2021.11.27
[Spring] Scheduler Lock  (0) 2021.11.25
[Spring] @ConfigurationProperties  (0) 2021.08.02
[Spring] @Value  (0) 2021.08.02
[Spring] Sharding  (0) 2021.07.07

+ Recent posts