MySql의 Master + Slave 구조에서 Spring의 DataSource를 선택하기 위해 Spring 자체에서 제공하는 RoutingDataSource와 각 Master의 DataSource와 Slave DataSource를 작성하는 방법 2개 중 어떤 것이 성능이 괜찮을지 테스트하게 되었다.
당연히 나는 이 결과가 별로 차이가 없기를 바랬다. 더 쉬운게 성능까지 더 좋다면 더 좋은거고! 근데 그러기는 쉽지 않으니까!
Spring Boot + MySql(Master + Slave) 구성 (Feat. Mybatis) 이전 글에 docker를 사용하여 로컬에 세팅하는 방법을 통해 MySql을 세팅해두자.
@Configuration
@MapperScan(value="com.example.project.mapper", sqlSessionFactoryRef = "sqlSessionFactory")
public class DataSourceConfig {
public static final String MASTER = "masterDataSource";
public static final String SLAVE = "slaveDataSource";
@Bean
@ConfigurationProperties(prefix = "spring.master.datasource")
public DataSource masterDataSource(){
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
@Bean
@ConfigurationProperties(prefix = "spring.slave.datasource")
public DataSource slaveDataSource(){
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
//@Bean
@Profile(value = "local")
public DataSourceInitializer dataSourceInitializer(@Qualifier(MASTER) DataSource master, ApplicationContext applicationContext){
ResourceDatabasePopulator resourceDatabasePopulator = new ResourceDatabasePopulator();
resourceDatabasePopulator.addScript(applicationContext.getResource("classpath:db/h2/schema.sql"));
// resourceDatabasePopulator.addScript(applicationContext.getResource("classpath:db/h2/data.sql"));
DataSourceInitializer dataSourceInitializer = new DataSourceInitializer();
dataSourceInitializer.setDataSource(master);
dataSourceInitializer.setDatabasePopulator(resourceDatabasePopulator);
return dataSourceInitializer;
}
@Bean
@DependsOn({MASTER, SLAVE})
public RoutingDataSource routingDataSource(@Qualifier(MASTER) DataSource master, @Qualifier(SLAVE) DataSource slave){
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(MASTER, master);
targetDataSources.put(SLAVE, slave);
RoutingDataSource routingDataSource = new RoutingDataSource();
routingDataSource.setTargetDataSources(targetDataSources);
routingDataSource.setDefaultTargetDataSource(master); //기본은 master
return routingDataSource;
}
@Bean
public LazyConnectionDataSourceProxy lazyDataSource(RoutingDataSource routingDataSource){
return new LazyConnectionDataSourceProxy(routingDataSource);
}
@Bean
public PlatformTransactionManager transactionManager(LazyConnectionDataSourceProxy routingDataSource){
return new DataSourceTransactionManager(routingDataSource);
}
@Bean
public SqlSessionFactory sqlSessionFactory(LazyConnectionDataSourceProxy dataSource, ApplicationContext applicationContext) throws Exception{
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
sessionFactory.setMapperLocations(applicationContext.getResources("classpath:db/mapper/**/*.xml"));
sessionFactory.setConfigLocation(applicationContext.getResource("classpath:db/mybatis-config.xml"));
return sessionFactory.getObject();
}
}
@Slf4j
public class RoutingDataSource extends AbstractRoutingDataSource{
@Override
protected Object determineCurrentLookupKey() {
String routing = TransactionSynchronizationManager.isCurrentTransactionReadOnly() ? DataSourceConfig.SLAVE : DataSourceConfig.MASTER;
log.debug("routing ? {}", routing);
return routing;
}
}
@Mapper
public interface TesttMapper {
//select
void sel(int i);
//insert
void ins(String str);
//delete
void del();
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.project.mapper.member.TesttMapper">
<insert id="ins" parameterType="String">
insert into testt (name) values (#{name})
</insert>
<select id="sel" parameterType="Integer" resultType="String">
select `name` from testt where id = #{id}
</select>
<delete id="del">
delete from testt
</delete>
</mapper>
@Configuration
@MapperScan(value="com.example.project.test.master", sqlSessionFactoryRef = "masterSqlSessionFactory")
@EnableTransactionManagement
public class MasterConfig {
public static final String MASTER = "masterDataSource";
@Bean("masterTest")
@ConfigurationProperties(prefix = "spring.master.datasource1")
public DataSource masterDataSource(){
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
@Bean("masterSqlSessionFactory")
public SqlSessionFactory masterSqlSessionFactory(@Qualifier("masterTest") DataSource dataSource, ApplicationContext applicationContext) throws Exception{
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
sessionFactory.setMapperLocations(applicationContext.getResources("classpath:db/mapper/**/*.xml"));
sessionFactory.setConfigLocation(applicationContext.getResource("classpath:db/mybatis-config.xml"));
return sessionFactory.getObject();
}
}
@Configuration
@MapperScan(value="com.example.project.test.slave", sqlSessionFactoryRef = "slaveSqlSessionFactory")
@EnableTransactionManagement
public class SlaveConfig {
public static final String SLAVE = "slaveDataSource";
@Bean("slaveTest")
@ConfigurationProperties(prefix = "spring.slave.datasource1")
public DataSource slaveDataSource(){
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
@Bean("slaveSqlSessionFactory")
public SqlSessionFactory slaveSqlSessionFactory(@Qualifier("slaveTest") DataSource dataSource, ApplicationContext applicationContext) throws Exception{
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
sessionFactory.setMapperLocations(applicationContext.getResources("classpath:db/mapper/**/*.xml"));
sessionFactory.setConfigLocation(applicationContext.getResource("classpath:db/mybatis-config.xml"));
return sessionFactory.getObject();
}
}
@Mapper
public interface Testt2MasterMapper {
//insert
void ins(String name);
//delete
void del();
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.project.test.master.Testt2MasterMapper">
<insert id="ins" parameterType="String">
insert into testt2 (name) values (#{name})
</insert>
<delete id="del">
delete from testt2
</delete>
</mapper>
@Mapper
public interface Testt2SlaveMapper {
void sel(int i);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.project.test.slave.Testt2SlaveMapper">
<select id="sel" parameterType="Integer" resultType="String">
select `name` from testt2 where id = #{id}
</select>
</mapper>
public interface TestService {
...
/**
* 테스트
*/
void ins1(int i);
void sel1(int i);
void ins2(int i);
void sel2(int i);
}
@Service
@RequiredArgsConstructor
@Slf4j
@Transactional(readOnly = true)
public class TestServiceImpl implements TestService{
private final TesttMapper testtMapper;
private final Testt2MasterMapper testt2MasterMapper;
private final Testt2SlaveMapper testt2SlaveMapper;
@Transactional
@Override
public void ins1(int i) {
testtMapper.ins("test"+i);
}
@Override
public void sel1(int i) {
testtMapper.sel(i);
}
@Transactional
@Override
public void ins2(int i) {
testt2MasterMapper.ins("2test"+i);
}
@Override
public void sel2(int i) {
testt2SlaveMapper.sel(i);
}
}
@SpringBootTest
public class TestServiceTest {
@Autowired
private LoginService loginService;
@Autowired
private TesttMapper testtMapper;
@Autowired
private Testt2MasterMapper testt2MasterMapper;
@Autowired
private Testt2SlaveMapper testt2SlaveMapper;
@Test
@RepeatedTest(value = 10000)
void insTest(RepetitionInfo info){
loginService.ins1(info.getCurrentRepetition());
}
@Test
@RepeatedTest(value = 10000)
void selTest(RepetitionInfo info){
loginService.sel1(info.getCurrentRepetition());
}
@Test
@RepeatedTest(value = 10000)
void insTest2(RepetitionInfo info){
loginService.ins2(info.getCurrentRepetition());
}
@Test
@RepeatedTest(value = 10000)
void selTest2(RepetitionInfo info){
loginService.sel2(info.getCurrentRepetition());
}
}
Spring Boot + MySql(Master + Slave) 구성 (Feat. Mybatis) 이전 글을 참고하여 cmd 창에서 확인해보면 된다.
이런식으로 write는 master로 read는 slave로 가는 것을 확인!
결과는 참혹하다... insert의 경우 거의 차이가 없지만 select의 경우 2배 이상 차이가 난다... 쿼리 상 readonly 옵션 값을 db에도 적용시키고 있어서 그런거 같은데... 이 점을 동일하게 만들어서 테스트해보고 싶다.
JPA를 쓸경우에는 RoutingDataSource가 좋을거 같지만 MyBatis를 쓸 경우 Mapper를 개발자가 직접 따로 작성할 수 있기 때문에 DataSource를 직접 주입해서 따로 쓰는게 맞는거 같다.