-
Notifications
You must be signed in to change notification settings - Fork 0
Multi DataSource Transaction 처리 방안
arawn edited this page Jul 6, 2012
·
1 revision
- 일자 : 2012-07-05
- 시간 : 19:30 ~ 21:00
- 장소 : 강남 유쾌한발상(http://www.funidea.net/gn/map.htm)
- 참가자 : 조승모, 김태우, 최용은, 임대웅, 박용권, 석종일, 김기훈, 김지헌, 박가람
- 봄싹에 모임정보 보러가기
- sharding, multi datasource를 사용할 경우 트랜잭션 처리 노하우 공유
- Annotation base와 aspect point-cut 처리 방식 등 여러 가지로 트랜잭션을 처리 가능한데 대규모 사이트에서 어떤 방식이 가장 효과적이었는지?
- 동접자가 1만명 정도 사이트에서 발생한 이야기 ( Spring + iBatis - Tomcat - MySql )
- 데이터베이스 분리에 대한 요구 상황발생(하나로 운영하던 DB를 2개로 분리)
- 2개의 DataSource, 2개의 TransactionManager, 그리고 transactionManager를 묶어주는 ChainedTransactionManager를 적용
- 내부 테스트 통과 (최대한 운영 환경과 유사한 상황에서 부하 테스트 포함)
- 운영 반영 후 두 DataSource간 데이터 불일치(트랜잭션 원자성 실패) 발생, DB Lock 발생
- 현재는 DB 링크(DB link)로 처리
OpenSource JTA implementation
> Bitronix
- 잘 정리된 문서가 있다. (제품에 대한 문서외 글로벌 트랜잭션에 대한 Architecture 관련 문서도 있다.)
- Jetty, Tomcat, SpringFramework과 통합하기 좋은 솔루션을 제공
- Spring 포럼에서 많이 보이는 프레임워크
> Atomikos TransactionsEssentials®
- 상용제품에서 무료로 오픈되었다.
- 잘 정리된 문서가 있다.
- 과거 상용으로서 제품이 팔렸을 정도면 꽤 안정적일거라고 추측해본다.
- 더 다양한 기능으로 무장된 Atomikos ExtremeTransactions® 제품은 상용으로 판매 중
> SimpleJTA - A Simple Java Transaction Manager
- 국내에서는 관련 자료를 찾아보기 힘들다.
> JBossTS
- JBoss는 WAS에서 제공하는 만큼 성숙된 모습을 보여준다고 한다.
- 국내에서는 관련 자료를 찾아보기 힘들다.
> JOTM - Java Open Transaction Manager + XAPool
- 검색에서 가장 많이 나온다.
- 국내에서도 적용한 글들이 많이 나온다.
- XAPool 1.5.0에는 버그가 있다. - XAPool 이용 시 NullPointerException
- 2005년 이후로 개발되지 않는 것 같다. (사망?)
> tyrex
- 국내에서는 관련 자료를 찾아보기 힘들다.
- 2005년 이후로 개발되지 않는 것 같다. (사망?)
- 총 사용자 1천, 평균 접속자 500명
- DB는 Oracle 2개, MS-SQL 1개 사용
- JOTM 적용, 큰 문제없이 지금까지 잘 사용 중
- Oracle 1개, Sybase 1개 사용
- JOTM 적용, 트랜잭션은 문제 없었음
- Sybase에서 ConnectionPool 관련 오류가 24시간 주기로 발생 ( XAPool or JDBC Driver 문제로 추측만 난무 )
- JOTM, XAPool 걷어내고, 작업이 많지 않은 Oracle은 수동으로 제어
- ChainedTransactionManager과 유사한 방법으로 n개의 DataSource를 다루는 PlatformTransactionManager를 구현해서 적용 ( 큰 문제없이 사용 중, 사용량이 많지 않은 사이트 )
- 업무량이 많지 않은 특정 DataSource는 Programmatic transaction management로 처리하고, 많은쪽을 선언적 트랜잭션으로 처리
- 운영과 개발이 함께가는 경우 점직적으로 확대 적용하는것도 방법
- 원인을 찾아보기 힘든 경우에는 좋은 모니터링 도구의 도움을 받는게 좋다( 제니퍼 )
- bitrace와 같은 프레임워크를 사용해서 직접 특정 영역한 모니터링을 해보는것도 좋다
- bitrace를 사용해보니 서비스 중인 애플리케이션의 코드를 손대지 않아도 로그를 남기고 추적 할 수 있어서 좋았다
- DB Link를 통해서 분산 트랜잭션을 처리하는 것도 하나의 좋은 방법이다
- 경험상에 의하면 JMS와 같이 다양한 리소스에 대해 처리하는게 아니라면 DB Link를 통해서 로컬 트랜잭션으로 처리하는게 더 쉽고 안전한것 같
- Q: 스프링을 통해서 중첩 트랜잭션을 처리하는 방법은 있으나, 적용사례를 찾아보기 힘들어 섣불리 적용하기 힘들다.
- A: 스프링 트랜잭션 서비스는 충분히 성숙되어 있다. 테스트를 통해서 확인 후 적용해보는게 어떻느냐?
- Q : 하나의 트랜잭션으로 n개의 DataSource를 사용 할 수 있느냐?
- A : SpringSource의 Team Blog에 보면 DYNAMIC DATASOURCE ROUTING에 대한 아티클이 있다.
- Java Transaction Design Pattern
- Transaction Management
- apache - Dynamic Datasource
- Oracle DB 링크란 뭘까나?
- MS-SQL - Oracle, Procedure에 로직이 구현되어 있음
ChainedTransactionManager에 대한 의견
ChainedTransactionManager의 소스 코드를 검색해서 찾아봤습니다.
package com.googlecode.goodsamples.spring.support;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionException;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionSynchronizationManager;
/**
* <p>
* This source comes from <a href ="http://jira.springframework.org/browse/SPR-6237">here</a>.
* </p>
*/
public class ChainedTransactionManager implements PlatformTransactionManager {
protected Log logger = LogFactory.getLog(getClass());
private List<PlatformTransactionManager> transactionManagers = new ArrayList<PlatformTransactionManager>();
public void setTransactionManagers(
List<PlatformTransactionManager> transactionManagers) {
this.transactionManagers = transactionManagers;
}
@Override
public TransactionStatus getTransaction(TransactionDefinition definition)
throws TransactionException {
MultiTransactionStatus mts = new MultiTransactionStatus(
transactionManagers.get(0));
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
TransactionSynchronizationManager.initSynchronization();
mts.setNewSynchonization();
}
for (PlatformTransactionManager transactionManager : transactionManagers) {
mts.getTransactionStatuses().put(transactionManager,
transactionManager.getTransaction(definition));
}
return mts;
}
@Override
public void commit(TransactionStatus status) throws TransactionException {
boolean commit = true;
Exception commitException = null;
PlatformTransactionManager commitExceptionTransactionManager = null;
for (int i = transactionManagers.size() - 1; i >= 0; i--) {
PlatformTransactionManager transactionManager = transactionManagers
.get(i);
if (commit) {
try {
transactionManager.commit(((MultiTransactionStatus) status)
.getTransactionStatuses().get(transactionManager));
} catch (Exception ex) {
commit = false;
commitException = ex;
commitExceptionTransactionManager = transactionManager;
}
} else {
try {
transactionManager
.rollback(((MultiTransactionStatus) status)
.getTransactionStatuses().get(
transactionManager));
} catch (Exception ex) {
logger.warn("Rollback exception (after commit) ("
+ transactionManager + ") " + ex.getMessage(), ex);
}
}
}
if (((MultiTransactionStatus) status).isNewSynchonization()) {
TransactionSynchronizationManager.clear();
}
if (commitException != null) {
throw new RuntimeException("Commit exception ("
+ commitExceptionTransactionManager + ") "
+ commitException.getMessage(), commitException);
}
}
@Override
public void rollback(TransactionStatus status) throws TransactionException {
Exception rollbackException = null;
PlatformTransactionManager rollbackExceptionTransactionManager = null;
for (int i = transactionManagers.size() - 1; i >= 0; i--) {
PlatformTransactionManager transactionManager = transactionManagers
.get(i);
try {
transactionManager.rollback(((MultiTransactionStatus) status)
.getTransactionStatuses().get(transactionManager));
} catch (Exception ex) {
if (rollbackException == null) {
rollbackException = ex;
rollbackExceptionTransactionManager = transactionManager;
} else {
logger.warn("Rollback exception (" + transactionManager
+ ") " + ex.getMessage(), ex);
}
}
}
if (((MultiTransactionStatus) status).isNewSynchonization()) {
TransactionSynchronizationManager.clear();
}
if (rollbackException != null) {
throw new RuntimeException("Rollback exception ("
+ rollbackExceptionTransactionManager + ") "
+ rollbackException.getMessage(), rollbackException);
}
}
}제가 보기에는 위 소스는 트랜잭션의 원자성을 안전하게 보장하기에는 문제가 있다고 생각합니다.
A와 B라는 이름을 가진 2개의 transactionManager를 사용한다고 가정하고, commit 메소드를 호출 했을 때 다음과 같은 동작을 하는 것으로 보입니다.
- 상황 1: A 트랜잭션 커밋 -> 성공 -> B 트랜잭션 커밋 -> 성공 - > 종료
- 상황 2: A 트랜잭션 커밋 -> 실패 -> B 트랜잭션 롤백
그런데, 아래와 같은 상황이라면 원자성이 깨질것으로 보이네요.
- 상황 3: A 트랜잭션 커밋 -> 성공 -> B 트랜잭션 커밋 -> 실패 -> 예외
위와 같다면 이미 A는 성공적으로 커밋을 하고 DB에 데이터가 들어간 상황일겁니다.