Skip to content

Multi DataSource Transaction 처리 방안

arawn edited this page Jul 6, 2012 · 1 revision

모임정보

주제 : Multi DataSource Transaction 처리 방안?

  • 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에서 제공하는 만큼 성숙된 모습을 보여준다고 한다.

> GeronimoTM/Jencks

  • 국내에서는 관련 자료를 찾아보기 힘들다.

> JOTM - Java Open Transaction Manager + XAPool

  • 검색에서 가장 많이 나온다.
  • 국내에서도 적용한 글들이 많이 나온다.
  • XAPool 1.5.0에는 버그가 있다. - XAPool 이용 시 NullPointerException
  • 2005년 이후로 개발되지 않는 것 같다. (사망?)

> tyrex

  • 국내에서는 관련 자료를 찾아보기 힘들다.
  • 2005년 이후로 개발되지 않는 것 같다. (사망?)

적용사례

XXXXX 기업 업무시스템

  • 총 사용자 1천, 평균 접속자 500명
  • DB는 Oracle 2개, MS-SQL 1개 사용
  • JOTM 적용, 큰 문제없이 지금까지 잘 사용 중

XXXX 서비스

  • 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에 대한 아티클이 있다.

참고문헌

기타

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에 데이터가 들어간 상황일겁니다.