반응형
들어가며
설명
- Replication 기본 설명 참고
- @Transactional(readOnly = true)일 경우 Slave DB에 접속하고, @Transactional일 경우 Master DB에 접속하는 예제를 설명한다.
- 예제를 위해 JPA를 사용한다.
- Replication DB 구성을 위해 Master 1대, Slave 2대를 MySQL로 구성한다.
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] Thymeleaf (0) | 2021.06.26 |