본문 바로가기
블로그 이미지

방문해 주셔서 감사합니다! 항상 행복하세요!

  
   - 문의사항은 메일 또는 댓글로 언제든 연락주세요.
   - "해줘","답 내놔" 같은 질문은 답변드리지 않습니다.
   - 메일주소 : lts06069@naver.com


몽고DB/Java 몽고DB

몽고DB 트랜잭션을 위한 리플리카 셋, 적용 테스트(Mongodb transaction, Mongodb replica set)

야근없는 행복한 삶을 위해 ~
by 마샤와 곰 2019. 11. 26.

 

몽고DB 4.0 이상 버전부터 트랜잭션 지원이 가능하여 졌다.

몽고DB의 오랜 단점을 지워주는 멋진기능인데...이게 일반적인 몽고DB서버에 적용하는 것은 불가능 하다.

https://docs.mongodb.com/manual/reference/method/Session.startTransaction/

 

위 글의 핵심적인 내용은 몽고db에서의 트랜잭션은 리플리카셋 환경에서만 지원된다는 점 이다.

 * 리플리카 = 레플리카 = Replica

리플리카셋은 쉽게 말하면 백업 저장소의 개념이며 최소한 마스터서버, 슬레이브서버가 각 1개씩 존재 하여야 한다. 

다른표현으로는...관계형 DB에서 주로 사용되는 "데이터베이스 이중화" 라는 말과 비슷한 개념이다.

 

마스터서버, 슬레이브서버 등으로 데이터베이스가 서로 최소 2개로 연결 되어야 트랜젝션 처리가 가능하다.

만약, 몽고DB 환경이 리플리카셋으로 이루어지지 않았다면 아래와 같은 오류를 만나게 된다.

  * 1개로만 동작하는 몽고DB에 사용하는 경우

1개만 동작하는 몽고db에 트랜잭션 코드를 실행하면 볼 수 있는 사진

* 오류내용 : ssession are not supported by mongodb cluster to wich this client is connected..

 

 

아무튼 리플리카셋 구성은 그렇게 어렵지는 않으며, 몽고DB에서의 리플리카셋은 3가지로 구성되어 진다.

 

1. 마스터 서버(1개) : 메인 데이터베이스 서버, 사용자가 접속하는 대상 서버

2. 슬레이브 서버(n개) : 서브 형태의 서버, 마스터 서버와의 데이터를 동기화하는 서버

3. 아비터 서버(1개) : 감시용 서버, 문제가 생긴 서버를 대신해서 동작할 다른 서버를 지정하는 등의 역할을 하는 서버

 

대충 서로의 역할은 요런모양으로 이루어진다.

 

여기서는 마스터, 슬레이브, 아비터 서버 각각 1개씩 만들어 적용하여 보았다.

* 슬레이브 서버는 여러개 둘 수 있다.

 

* 리플리카셋 설정 단계 (데이터베이스 작업환경 : 우분투)

1. 몽고db를 받는다. (wegt을 활용해서)

  - 몽고db를 동작시킬 디렉토리를 만든 후 파일을 받아서 압축을 해제한다. (여기서는 /home/mongodb/mongo/)

 

2. 각각 동작시킬 설정파일과 디렉토리를 만든다.

   각각 설정파일에서의 다른 부분은 사용 할 포트번호와 데이터베이스 저장위치, 로그폴더 위치가 전부이다.

  1) 마스터용 디렉토리, 설정파일, 로그폴더 만들기

   - /home/mongodb/master/master.conf

   - /home/mongodb/master/log

  2) 슬레이브용 디렉토리, 설정파일, 로그폴더 만들기

   - /home/mongodb/slave1/slave1.conf

   - /home/mongodb/slave1/log

  3) 아비터용 디렉토리, 설정파일, 로그폴더 만들기

   - /home/mongodb/arbiter/arbiter.conf

   - /home/mongodb/arbiter/log

#설정파일 예제

storage:
  dbPath: /home/mongodb/master  #데이터베이스 경로, 각각 다른경로를 지정해줘야한다.
  journal:
    enabled: true  #아비터 관련하여 여기 값을 false로 해도 된다라는 글은 무시하자.

systemLog:
  destination: file
  logAppend: true
  path: /var/log/mongodb/mongod.log  #로그 경로, 미리 디렉토리를 만들어둬야한다. 안만들면 오류!

net:
  port: 26016  #포트는 마스터, 슬레이브, 아비터 각각 따로 주어야 한다.
  bindIp: 127.0.0.1
  
processManagement:
  timeZoneInfo: /usr/share/zoneinfo
  fork: true 

replication:
  replSetName: "replica"    #여기이름 기억하자!!!!!!! 꼭!!!!

  * 간혹 구글링을 하면 journal 값을 false로 줘도 된다는 글이 있는데 무조건 true로 주어야 한다. (몽고DB 4.0)

    - 관련글 : https://docs.mongodb.com/manual/core/journaling/

  * 각각의 디렉토리는 미리 만들어 두어야한다. 디렉토리가 없으면 오류가 난다.(물론 권한도 확인해야 한다.)

 


3. 마스터로 사용할 몽고db를 config 파일과 함께 올린다.

  1) /home/mongodb/mongo/bin 디렉토리로 이동

  2) mongod 명령어를 --config 옵션과 함께 실행한다.

  * 주의사항 : 데이터베이스 디렉토리, 로그 디렉토리는 미리 만들어야 하며 권한도 확인 하여야 한다.

이런식으로 명령을 입력하면 된다.


4. 위 방식으로 노예로 사용할 서버와 아비터로 활용할 서버도 올려준다.

 

5. 해당 서비스가 전부 동작중인지 확인하여 본다. netstat -tnlp를 활용해서 서비스 포트가 열려있는지 보면 된다.

 

6. 마스터용 몽고db에 접속한다.

  1) /home/mongodb/mongo/bin 디렉토리로 이동

  2) 명령어 : ./mongo --port 마스터용포트

 

7. 콘솔창에서 아래 내용을 입력한다.

 *  보기 편하게 풀어서 썼지만, 명령어를 실행 할 때는 1줄로 입력하자.

rsconf = {
  _id : "config파일에서입력한 이름(맨아래)",
  members : [ 
     {_id:0, host:"127.0.0.1:마스터용포트번호", priority:2},
     {_id:1, host:"127.0.0.1:슬레이브용포트번호", priority:1},
     {_id:2, host:"127.0.0.1:아비터용포트번호",, priority:1, arbiterOnly: true}
  ]
};

  * 주의사항1 : 맨 처음 입력한 _id값은 conf 파일에서 입력한 replication의 replSetName과 일치하여야 한다.

  * 주의사항2

    - priority 값을 주지않으면 기본으로 모두 1을 가지게 된다.

    - 1인 경우에 서버가 재부팅하거나 마스터가 죽은 경우 다시 키면 마스터로 선정한 서버가 마스터가 되돌아 오지 않는다.

 

8. 입력 이후에 아래 명령어를 콘솔에 입력한다.

명령어 : rs.initiate(rsconf);

위 단계에서 오류가 없다면 콘솔창이 아래 사진처럼 모양이 바뀐다.

첫번째는 입력한 replSetName이며 PRIMARY는 마스터를 의미한다.

 

9. 상태가 잘 올라와있는지 아래 명령어를 콘솔에 확인하여본다.

명령어 : rs.status();

 

 

여기까지 하였다면 마스터 - 슬레이브 - 아비터 세팅이 끝난 것 이다.

마스터, 슬레이브, 아비터에 관한 좋은 글은 검색하면 많이 나온다.

 

 

리플리카셋 환경이 구성된 뒤에 트랜잭션 코드를 실행하여보면 정상적으로 동작함을 확인 할 수 있다.

오...transaction부분에서 값이 boolean 형태로 바뀌었다!!!!! 에러가 안나!!

 

트랜잭션 테스트에 사용된 실제 코드이다.

java에서는 Client 객체를 가져와서 데이터를 변경하는 동안 트랜잭션 session 객체를 데이터를 조작하는 행위에 넣어주어야 동작한다.

여기서는 insertOne이라는 메소드에 적용시켜 보았다.

먼저 메인 클래스 이다.

import java.util.Date;

import org.bson.Document;
import org.bson.types.ObjectId;

import com.mongodb.ReadConcern;
import com.mongodb.ReadPreference;
import com.mongodb.TransactionOptions;
import com.mongodb.WriteConcern;
import com.mongodb.client.ClientSession;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.TransactionBody;


public class App extends MyMongoBody{
    public static void main( String[] args ) {
        App app = new App();
        app.init();
        ClientSession session = app.mongoClient.startSession();

        try {

            TransactionOptions txnOptions = TransactionOptions.builder()
                .readPreference(ReadPreference.primary())
                .readConcern(ReadConcern.LOCAL)
                .writeConcern(WriteConcern.MAJORITY)
                .build();
            //데이터 베이스는 admin, 컬렉션은 test이다.
            session.withTransaction(new TransactionBody<String>() {
                @Override
                public String execute() {
                    MongoDatabase db = app.mongoClient.getDatabase("admin");
                    //아래 withWriteConcern이 트랜잭션을 고려하겠다는 옵션이다.
                    MongoCollection<Document> target = db.getCollection("test").withWriteConcern(WriteConcern.MAJORITY);
                    Document contents = new Document();
                    contents.put("_id", new ObjectId(new Date()));
                    contents.put("asdf", "asdf");
                    contents.put("basdfb1q", 1234);
                    contents.put("basdfb2q", 5555);						
                    //저장할 때 session객체도 넣어줘야한다. 넣지 않으면 걍 insert한다.
                    target.insertOne(session,contents);  

                    Integer.parseInt("abcd"); //여기서 오류를 주어보자. 롤백된다.

                    contents = new Document();
                    contents.put("_id", new ObjectId(new Date()));
                    contents.put("asdf", "asdf");
                    contents.put("basdfb1q", 1234);
                    contents.put("basdfb2q", 5555);						
                    target.insertOne(session,contents);
                    return "SUCC";
                }
            }, txnOptions);        	
            System.out.println("transaction : " + session.hasActiveTransaction());
            session.commitTransaction();
        } catch (Exception e) {
            e.printStackTrace();
            session.abortTransaction();
        } finally{
            app.close();
            System.exit(0);
        }


    }
}

 

상속받아서 사용한 몽고db접속용 코드이다.

몽고db는 터널링 방식으로 접속하였으며, 몽고db서버에는 별도로 인증을 거치지는 않았다. (터널링으로 하니까..)

import org.springframework.data.mongodb.core.MongoTemplate;
import com.mongodb.ConnectionString;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoClient;
import com.mongodb.MongoClientSettings;

import com.jcraft.jsch.JSch;
import com.jcraft.jsch.Session;

public abstract class MyMongoBody {
    //터널링 방식으로 접속 하였다
    private static final String SSH_USER = "SSH접속아이디";
    private static final String SSH_PASSWORD = "비밀번호";
    private static final String SSH_HOST = "아이피";
    private static final Integer SSH_PORT = 22;  //ssh 포트
    private static Session SSH_SESSION;
    
    private static final int VIRTUAL_PORT = 11567;  //가상포트, 포트포워딩에서 사용할 가상포트
    private static final String LOCAL = "127.0.0.1";  //몽고db에 접속하기 위한 주소
    
    public MongoClient mongoClient = null;
    public MongoTemplate template = null;
    
    public void init() {
        try {

            java.util.Properties config = new java.util.Properties();
            config.put("StrictHostKeyChecking", "no");
            JSch jsch = new JSch();
            SSH_SESSION = null;
            SSH_SESSION = jsch.getSession(SSH_USER, SSH_HOST, SSH_PORT);
            SSH_SESSION.setPassword(SSH_PASSWORD);
            SSH_SESSION.setConfig(config);
            SSH_SESSION.connect();
            
            //26016은 몽고db 포트이다. 마스터 포트를 의미한다.
            //VIRTUAL_PORT는 포트포워딩에 사용되는 포트로 26016과 겹치면안된다.
            //이러한 가상포트는 아무거나 줘도 된다. 여기서는 11567을 사용하였다.
            SSH_SESSION.setPortForwardingL(VIRTUAL_PORT, LOCAL, 26016);

            
            final String combineUrl = "mongodb://"+LOCAL+":"+VIRTUAL_PORT+"/admin";
            
            ConnectionString connString = new ConnectionString(combineUrl);            
            MongoClientSettings settings = MongoClientSettings.builder()
                    .applyConnectionString(connString)
                    .retryWrites(true)
                    .build();

            mongoClient = MongoClients.create(settings);  //몽고클라이언트, 트랜잭션시 사용된다.
            template = new MongoTemplate(mongoClient, "admin");  //몽고템플릿, 트랜잭션시에 사용되지는 않는다.
            
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    public void close(){
        try {
            template = null;
            mongoClient.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
}

 

사용한 maven 저장소 라이브러리이다.

		<dependency>
			<groupId>org.mongodb</groupId>
			<artifactId>mongo-java-driver</artifactId>
			<version>3.11.1</version>
		</dependency>

		<dependency>
			<groupId>org.springframework.data</groupId>
			<artifactId>spring-data-mongodb</artifactId>
			<version>2.2.1.RELEASE</version>
		</dependency>

		<dependency>
			<groupId>com.jcraft</groupId>
			<artifactId>jsch</artifactId>
			<version>0.1.55</version>
		</dependency>

 

* 사용한 config 파일 목록 첨부

컨피그파일목록.7z
0.00MB

 

 

 

반응형
* 위 에니메이션은 Html의 캔버스(canvas)기반으로 동작하는 기능 입니다. Html 캔버스 튜토리얼 도 한번 살펴보세요~ :)
* 직접 만든 Html 캔버스 애니메이션 도 한번 살펴보세요~ :)

댓글