JTA 로 JPA 다중 DataSource 트랜잭션 처리 하기
Last updated
Last updated
Spring Boot 2.x 에서는 2가지 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개 모두 갱신해줘야 한다. 하나라도 실패하면 실패로 간주해야 한다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
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
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
@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
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 정상 수행되었다고 불수 있음.
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
{"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"}]}