[Spring boot] 분산 트랜젝션

2021. 12. 7. 11:54프로그래밍/web

분산 트랜젝션이란 ? 

2개 그 이상의 네트워크 상의 시스템 간의 트랜잭션. 
2개의 Phase Commit
으로 분산 리소스간의 All or Nothing 보장

Spring Boot 내에서 XA protocol을 사용해서 two phase commit을 진행한다.
XA 트랜젝션 : XA 프로토콜을 사용하는 분산 트랜잭션

phase 1

phase 1에서는 prepare 요청을 보내고 모든 리소스 (DB)에게 커밋 준비 요청을 한다. 하나의 DB라도 OK가 오지않으면 Rollback을
실행해서 transaction의 ACID를 만족한다. 

phase 2

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("계좌가 존재하지 않습니다.");
		}
	}
}