JTA 로 JPA 다중 DataSource 트랜잭션 처리 하기

Spring Boot 2.x 에서는 2가지 JTA 구현방법을 정의하였다.

Spring Boot 2.x 의 JTA 구현

Atomikos:spring-boot-starter-jta-atomikos dependency 추가 또는

Bitronix:spring-boot-starter-jta-bitronix dependency 추가 하는 방식이다.

Spring Boot 2.3.0 부터 Bitronix 비권장으로 변경 되었다. 그래서 Atomikos 설정하는 부분만 언급한다.

여기서 JdbcTemplate 로 제일 기본적인 트랜젝션 transaction 만 다루어 본다.

아래와 같은 환경으로 예를 들어보자.

  • 우리는 2개의 Database가 있고 각각 test1 , test2 라고 한다.

  • 해당 Database 에서 모두 User 테이블이 있고 , 똑같게 유지되길 원한다. 그래서 client 로부터 들어온 name =aaa , age = 30 이라는 데이터를 각각 User 테이블에 Update 시 각각 User 테이블 2개 모두 갱신해줘야 한다. 하나라도 실패하면 실패로 간주해야 한다.

pom.xml 추가

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>

application.properties 설정

spring.jta.enabled=true

spring.jta.atomikos.datasource.primary.xa-properties.url=jdbc:mysql://localhost:3306/test1
spring.jta.atomikos.datasource.primary.xa-properties.user=root
spring.jta.atomikos.datasource.primary.xa-properties.password=12345678
spring.jta.atomikos.datasource.primary.xa-data-source-class-name=com.mysql.cj.jdbc.MysqlXADataSource
spring.jta.atomikos.datasource.primary.unique-resource-name=test1
spring.jta.atomikos.datasource.primary.max-pool-size=25
spring.jta.atomikos.datasource.primary.min-pool-size=3
spring.jta.atomikos.datasource.primary.max-lifetime=20000
spring.jta.atomikos.datasource.primary.borrow-connection-timeout=10000

spring.jta.atomikos.datasource.secondary.xa-properties.url=jdbc:mysql://localhost:3306/test2
spring.jta.atomikos.datasource.secondary.xa-properties.user=root
spring.jta.atomikos.datasource.secondary.xa-properties.password=12345678
spring.jta.atomikos.datasource.secondary.xa-data-source-class-name=com.mysql.cj.jdbc.MysqlXADataSource
spring.jta.atomikos.datasource.secondary.unique-resource-name=test2
spring.jta.atomikos.datasource.secondary.max-pool-size=25
spring.jta.atomikos.datasource.secondary.min-pool-size=3
spring.jta.atomikos.datasource.secondary.max-lifetime=20000
spring.jta.atomikos.datasource.secondary.borrow-connection-timeout=10000

또는 application.yml

spring:
    jta:
        atomikos:
            datasource:
                primary:
                    borrow-connection-timeout: 10000
                    max-lifetime: 20000
                    max-pool-size: 25
                    min-pool-size: 3
                    unique-resource-name: test1
                    xa-data-source-class-name: com.mysql.cj.jdbc.MysqlXADataSource
                    xa-properties:
                        password: root
                        url: jdbc:mysql://localhost:3306/test1
                        user: root
                secondary:
                    borrow-connection-timeout: 10000
                    max-lifetime: 20000
                    max-pool-size: 25
                    min-pool-size: 3
                    unique-resource-name: test2
                    xa-data-source-class-name: com.mysql.cj.jdbc.MysqlXADataSource
                    xa-properties:
                        password: root
                        url: jdbc:mysql://localhost:3306/test2
                        user: root
        enabled: true

다중 DataSource 설정 클래스 작성

@Configuration
public class DataSourceConfiguration {

    @Primary
    @Bean
    @ConfigurationProperties(prefix = "spring.jta.atomikos.datasource.primary")
    public DataSource primaryDataSource() {
        return new AtomikosDataSourceBean();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.jta.atomikos.datasource.secondary")
    public DataSource secondaryDataSource() {
        return new AtomikosDataSourceBean();
    }

    @Bean
    public JdbcTemplate primaryJdbcTemplate(@Qualifier("primaryDataSource") DataSource primaryDataSource) {
        return new JdbcTemplate(primaryDataSource);
    }

    @Bean
    public JdbcTemplate secondaryJdbcTemplate(@Qualifier("secondaryDataSource") DataSource secondaryDataSource) {
        return new JdbcTemplate(secondaryDataSource);
    }

}

주의! DataSource 소슷 관련 설정은 AtomikosDataSourceBean 을 사용했으므로 기존 JdbcTemplate 설정과 구분해서 설정해야 된다.

Service 를 작성

@Service
public class TestService {

    private JdbcTemplate primaryJdbcTemplate;
    private JdbcTemplate secondaryJdbcTemplate;

    public TestService(JdbcTemplate primaryJdbcTemplate, JdbcTemplate secondaryJdbcTemplate) {
        this.primaryJdbcTemplate = primaryJdbcTemplate;
        this.secondaryJdbcTemplate = secondaryJdbcTemplate;
    }

    @Transactional
    public void tx() {
        // test1 Database 수정
        primaryJdbcTemplate.update("update user set age = ? where name = ?", 30, "aaa");
        // test2 Database 수정
        secondaryJdbcTemplate.update("update user set age = ? where name = ?", 30, "aaa");
    }

    @Transactional
    public void tx2() {
        // test1 Database 수정
        primaryJdbcTemplate.update("update user set age = ? where name = ?", 40, "aaa");
        //가설:test2 Database 수정시 exception 발생 시키는 코드 
        throw new RuntimeException();
    }

}

테스트코드를 작성

@SpringBootTest(classes = TransactionalDemoApplication.class)
public class TransactionalDemoApplicationTests {

    @Autowired
    protected JdbcTemplate primaryJdbcTemplate;
    @Autowired
    protected JdbcTemplate secondaryJdbcTemplate;

    @Autowired
    private TestService testService;

    @Test
    public void test1() throws Exception {
        // 갱신성공
        testService.tx();
        Assertions.assertEquals(30, primaryJdbcTemplate.queryForObject("select age from user where name=?", Integer.class, "aaa"));
        Assertions.assertEquals(30, secondaryJdbcTemplate.queryForObject("select age from user where name=?", Integer.class, "aaa"));
    }

    @Test
    public void test2() throws Exception {
        // 갱신실패!
        try {
            testService.tx2();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 갱신 부분 실패!,test1 롤백 
            Assertions.assertEquals(30, primaryJdbcTemplate.queryForObject("select age from user where name=?", Integer.class, "aaa"));
            Assertions.assertEquals(30, secondaryJdbcTemplate.queryForObject("select age from user where name=?", Integer.class, "aaa"));
        }
    }

}

테스트 결과:

  • test1 : 모두 정상 update ,

  • test2: tx2 method() 가 test1 위 name = aaa 의 사용자 age = 40 로 갱신 후 예외 발생!, JTA 가 제대로 수행되었다면 age =30 으로 롤백 될것. 테스트 결과 age= 30 이므로 JTA 정상 수행되었다고 불수 있음.

Log 확인

2022-12-26 12:00:36.145  INFO 8868 --- [           main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.default_max_wait_time_on_shutdown = 9223372036854775807
2022-12-26 12:00:36.145  INFO 8868 --- [           main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.allow_subtransactions = true
2022-12-26 12:00:36.145  INFO 8868 --- [           main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.recovery_delay = 10000
2022-12-26 12:00:36.145  INFO 8868 --- [           main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.automatic_resource_registration = true
2022-12-26 12:00:36.145  INFO 8868 --- [           main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.oltp_max_retries = 5
2022-12-26 12:00:36.145  INFO 8868 --- [           main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.client_demarcation = false
2022-12-26 12:00:36.145  INFO 8868 --- [           main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.threaded_2pc = false
2022-12-26 12:00:36.145  INFO 8868 --- [           main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.serial_jta_transactions = true
2022-12-26 12:00:36.145  INFO 8868 --- [           main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.log_base_dir = /Users/didi/Documents/GitHub/SpringBoot-Learning/2.x/chapter3-12/transaction-logs
2022-12-26 12:00:36.145  INFO 8868 --- [           main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.rmi_export_class = none
2022-12-26 12:00:36.145  INFO 8868 --- [           main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.max_actives = 50
2022-12-26 12:00:36.145  INFO 8868 --- [           main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.checkpoint_interval = 500
2022-12-26 12:00:36.145  INFO 8868 --- [           main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.enable_logging = true
2022-12-26 12:00:36.145  INFO 8868 --- [           main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.log_base_name = tmlog
2022-12-26 12:00:36.146  INFO 8868 --- [           main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.max_timeout = 300000
2022-12-26 12:00:36.146  INFO 8868 --- [           main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.trust_client_tm = false
2022-12-26 12:00:36.146  INFO 8868 --- [           main] c.a.icatch.provider.imp.AssemblerImp     : USING: java.naming.factory.initial = com.sun.jndi.rmi.registry.RegistryContextFactory
2022-12-26 12:00:36.146  INFO 8868 --- [           main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.tm_unique_name = 127.0.0.1.tm
2022-12-26 12:00:36.146  INFO 8868 --- [           main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.forget_orphaned_log_entries_delay = 86400000
2022-12-26 12:00:36.146  INFO 8868 --- [           main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.oltp_retry_interval = 10000
2022-12-26 12:00:36.146  INFO 8868 --- [           main] c.a.icatch.provider.imp.AssemblerImp     : USING: java.naming.provider.url = rmi://localhost:1099
2022-12-26 12:00:36.146  INFO 8868 --- [           main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.force_shutdown_on_vm_exit = false
2022-12-26 12:00:36.146  INFO 8868 --- [           main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.default_jta_timeout = 10000
2022-12-26 12:00:36.147  INFO 8868 --- [           main] c.a.icatch.provider.imp.AssemblerImp     : Using default (local) logging and recovery...
2022-12-26 12:00:36.184  INFO 8868 --- [           main] c.a.d.xa.XATransactionalResource         : test1: refreshed XAResource
2022-12-26 12:00:36.203  INFO 8868 --- [           main] c.a.d.xa.XATransactionalResource         : test2: refreshed XAResource

transaction-logs 확인

{"id":"127.0.0.1.tm161226409083100001","wasCommitted":true,"participants":[{"uri":"127.0.0.1.tm1","state":"COMMITTING","expires":1612264100801,"resourceName":"test1"},{"uri":"127.0.0.1.tm2","state":"COMMITTING","expires":1612264100801,"resourceName":"test2"}]}
{"id":"127.0.0.1.tm161226409083100001","wasCommitted":true,"participants":[{"uri":"127.0.0.1.tm1","state":"TERMINATED","expires":1612264100804,"resourceName":"test1"},{"uri":"127.0.0.1.tm2","state":"TERMINATED","expires":1612264100804,"resourceName":"test2"}]}
{"id":"127.0.0.1.tm161226409092800002","wasCommitted":false,"participants":[{"uri":"127.0.0.1.tm3","state":"TERMINATED","expires":1612264100832,"resourceName":"test1"}]}

Last updated