[Spring boot] 분산 트랜젝션
2021. 12. 7. 11:54ㆍ프로그래밍/web
분산 트랜젝션이란 ?
2개 그 이상의 네트워크 상의 시스템 간의 트랜잭션.
2개의 Phase Commit으로 분산 리소스간의 All or Nothing 보장
2개의 Phase Commit으로 분산 리소스간의 All or Nothing 보장
Spring Boot 내에서 XA protocol을 사용해서 two phase commit을 진행한다.
XA 트랜젝션 : XA 프로토콜을 사용하는 분산 트랜잭션
phase 1에서는 prepare 요청을 보내고 모든 리소스 (DB)에게 커밋 준비 요청을 한다. 하나의 DB라도 OK가 오지않으면 Rollback을
실행해서 transaction의 ACID를 만족한다.
phase 2에서는 모든 DB에서 ok 응답이 올때까지 commit요청을 보내준다.
구현
1. maven 설정
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
2. XA 리소스 정보 추가
#Database1
spring.db1.datasource.xa-data-source-class-name=oracle.jdbc.xa.client.OracleXADataSource
spring.db1.datasource.xa-properties.url=jdbc:oracle:thin:@kosa3.iptime.org:11521:orcl
spring.db1.datasource.xa-properties.user=xxxxx
spring.db1.datasource.xa-properties.password=xxxxx
#Database2
spring.db2.datasource.xa-data-source-class-name=oracle.jdbc.xa.client.OracleXADataSource
spring.db2.datasource.xa-properties.url=jdbc:oracle:thin:@kosa1.iptime.org:50100:orcl
spring.db2.datasource.xa-properties.user=xxxx
spring.db2.datasource.xa-properties.password=xxxxx
3. 리소스 별 설정
DB1
package com.mycompany.webapp.config.jta;
import java.util.Properties;
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import com.atomikos.jdbc.AtomikosDataSourceBean;
@Configuration
@MapperScan(
basePackages="com.mycompany.webapp.dao.db2",
sqlSessionFactoryRef="db2SqlSessionFactory"
)
public class Db2Config {
@Value("${spring.db2.datasource.xa-data-source-class-name}")
private String xaDataSourceClassName;
@Value("${spring.db2.datasource.xa-properties.url}")
private String url;
@Value("${spring.db2.datasource.xa-properties.user}")
private String user;
@Value("${spring.db2.datasource.xa-properties.password}")
private String password;
public static final String DB2_DATASOURCE = "ds2DataSource";
@Bean(name=DB2_DATASOURCE)
public DataSource dataSource() {
AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
ds.setUniqueResourceName(DB2_DATASOURCE);
ds.setXaDataSourceClassName(xaDataSourceClassName);
Properties p = new Properties();
p.setProperty("URL", url);
p.setProperty("user", user);
p.setProperty("password", password);
ds.setXaProperties (p);
return ds;
}
@Bean(name="db2SqlSessionFactory")
public SqlSessionFactory sqlSessionFactory(
@Qualifier(DB2_DATASOURCE) DataSource dataSource) throws Exception {
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
PathMatchingResourcePatternResolver resover = new PathMatchingResourcePatternResolver();
sessionFactory.setMapperLocations(resover.getResources("classpath:mybatis/db2/*.xml"));
sessionFactory.setConfigLocation(resover.getResource("classpath:mybatis/mapper-config.xml"));
return sessionFactory.getObject();
}
}
DB2
package com.mycompany.webapp.config.jta;
import java.util.Properties;
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import com.atomikos.jdbc.AtomikosDataSourceBean;
@Configuration
@MapperScan(
basePackages="com.mycompany.webapp.dao.db1",
sqlSessionFactoryRef="db1SqlSessionFactory"
)
public class Db1Config {
@Value("${spring.db1.datasource.xa-data-source-class-name}")
private String xaDataSourceClassName;
@Value("${spring.db1.datasource.xa-properties.url}")
private String url;
@Value("${spring.db1.datasource.xa-properties.user}")
private String user;
@Value("${spring.db1.datasource.xa-properties.password}")
private String password;
public static final String DB1_DATASOURCE = "db1DataSource";
@Bean(name=DB1_DATASOURCE)
public DataSource dataSource() {
AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
ds.setUniqueResourceName(DB1_DATASOURCE);
ds.setXaDataSourceClassName(xaDataSourceClassName);
Properties p = new Properties();
p.setProperty("URL", url);
p.setProperty("user", user);
p.setProperty("password", password);
ds.setXaProperties(p);
return ds;
}
@Bean(name="db1SqlSessionFactory")
public SqlSessionFactory sqlSessionFactory(
@Qualifier(DB1_DATASOURCE) DataSource dataSource) throws Exception {
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
PathMatchingResourcePatternResolver resover = new PathMatchingResourcePatternResolver();
sessionFactory.setMapperLocations(resover.getResources("classpath:mybatis/db1/*.xml"));
sessionFactory.setConfigLocation(resover.getResource("classpath:mybatis/mapper-config.xml"));
return sessionFactory.getObject();
}
}
4. JTATransactional Manager 설정
package com.mycompany.webapp.config.jta;
import javax.transaction.UserTransaction;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.jta.JtaTransactionManager;
import com.atomikos.icatch.jta.UserTransactionImp;
import com.atomikos.icatch.jta.UserTransactionManager;
import lombok.extern.slf4j.Slf4j;
@Configuration
@Slf4j
public class JtaTransactionManagerConfig {
@Bean
public PlatformTransactionManager transactionManager() throws Exception {
log.info("transactionManager() 실행");
UserTransaction userTransaction = new UserTransactionImp();
userTransaction.setTransactionTimeout(10000);
UserTransactionManager userTransactionManager = new UserTransactionManager();
userTransactionManager.setForceShutdown(false);
JtaTransactionManager manager = new JtaTransactionManager(
userTransaction, userTransactionManager);
return manager;
}
}
5. Service Transactional 설정
package com.mycompany.webapp.service;
import java.util.List;
import javax.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.mycompany.webapp.dao.db1.Db1AccountDao;
import com.mycompany.webapp.dao.db2.Db2AccountDao;
import com.mycompany.webapp.dto.Account;
import com.mycompany.webapp.exception.NotFoundAccountException;
import lombok.extern.slf4j.Slf4j;
@Service
@Slf4j
public class AccountService {
@Resource
private Db1AccountDao db1AccountDao;
@Resource
private Db2AccountDao db2AccountDao;
public List<Account> getDb1Accounts() {
log.info("getDb1Accounts 실행");
List<Account> accounts = db1AccountDao.selectAll();
return accounts;
}
public List<Account> getDb2Accounts() {
log.info("getDb2Accounts 실행");
List<Account> accounts = db2AccountDao.selectAll();
return accounts;
}
//JTA 환경이 감지되면 Spring은 JtaTransactionManager를 자동으로 사용
@Transactional
public void accountTransfer(int fromAno, int toAno, int amount) {
log.info("accountTransfer 실행");
try {
//출금하기
Account fromAccount = db1AccountDao.selectByAno(fromAno);
fromAccount.setBalance(fromAccount.getBalance() - amount);
db1AccountDao.updateBalance(fromAccount);
//예금하기
Account toAccount = db2AccountDao.selectByAno(toAno);
toAccount.setBalance(toAccount.getBalance() + amount);
db2AccountDao.updateBalance(toAccount);
} catch(Exception e) {
throw new NotFoundAccountException("계좌가 존재하지 않습니다.");
}
}
}
'프로그래밍 > web' 카테고리의 다른 글
HTTP 자주 나오는 응답코드 정리 (0) | 2022.01.13 |
---|---|
[Spring]Cookie와 활용법(읽기, 생성 및 저장) (0) | 2021.09.20 |
[JAVA] abstract class vs interface (0) | 2021.08.02 |
[Spring] Spring MVC framework를 사용한 웹은 어떻게 작동할까? (0) | 2021.07.19 |
[Spring Boot] 댜양한 의존 관계 주입 DI 방법 (0) | 2021.07.01 |