반응형
Transaction
Spring Transaction 설정 방법
- 선언적 방법 :
@Transactional
를 method나 class에 붙인다
- 현재 spring 진형에서 선호하는 방식
- 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 설정
- default data source 설정 config bean 재외
- 이유: 실제로 저희가 직접 설정하고 싶은 replication된 master slave mysql 설정이 아닌, 기본적으로
application.yml
에 작성된 mysql 설정을 사용하는 config bean 이기 때문에 재외 해줘야 합니다.
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
public class ReplicationDataSourceConfig {
}
- 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에 설정해줍니다
- 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가 반환 되도록 하였습니다.
- 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 자동설정 기능 추가
- 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()));
}
}
참고
반응형
'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 |