본문 바로가기

인프라/MSA

[MSA] 분산 트랜잭션 - 2PC

2-Phase Commit (2PC)

출처: https://medium.com

 

2단계 커밋은 분산 데이터베이스 관리 시스템에서 트랜잭션의 일관성을 보장하기 위한 프로토콜입니다. MSA는 여러 독립적인 마이크로서비스로 구성되어 있으며, 이러한 서비스들 간의 트랜잭션 일관성을 유지하기 위해 2단계 커밋이 사용될 수 있습니다.

 

2PC의 원리

1. 준비 단계 (Prepare Phase)

- 트랜잭션 코디네이터(주로 분산 트랜잭션 매니저)는 모든 참여자 서비스에게 트랜잭션 수행에 동의할 것인지 물어봅니다.

- 각 서비스는 트랜잭션이 성공적으로 수행될 수 있는지 여부를 판단하고 준비 완료 시 코디네이터에게 알립니다.

- 만약 어떤 서비스라도 트랜잭션이 성공적으로 수행될 수 없다면 모든 참여자에게 롤백 명령이 전송됩니다.

 

2. 커밋 단계 (Commit Phase)

- 모든 참여자 서비스가 준비 완료를 보고하면, 코디네이터는 커밋 명령을 전송합니다.

- 각 서비스는 이 명령에 따라 트랜잭션을 커밋하거나 롤백합니다.

 

2PC의 한계

1. Blocking Issue

- 2PC는 참여자 서비스들이 트랜잭션에 참여하고 준비 완료 신호를 기다리는 동안 코디네이터가 블록될 수 있습니다. 특히, 하나의 서비스가 응답하지 않으면 전체 트랜잭션이 막힐 수 있습니다.

 

2. Single Point of Failure

- 코디네이터가 분산 환경에서 단일 장애 지점이 될 수 있습니다. 코디네이터의 장애는 전체 트랜잭션을 막을 수 있으며, 이는 시스템의 가용성과 내결함성에 영향을 미칠 수 있습니다.

 

3. 성능 저하

- 2PC는 트랜잭션 처리를 위해 여러 단계를 거쳐야 하기 때문에 성능 저하가 발생할 수 있습니다. 특히, 다수의 참여자 서비스가 있는 경우에는 트랜잭션 처리 시간이 증가할 수 있습니다.

 

2PC의 대안

1. 3단계 커밋 등의 확장된 프로토콜 사용

- 일부 시나리오에서는 3단계 커밋과 같은 더 복잡한 프로토콜을 사용하여 2PC의 일부 한계를 극복할 수 있습니다.

 

2. 분산 트랜잭션을 피하는 방법 고려

- 느슨하게 결합된 서비스 간의 통신을 통해 트랜잭션 일관성을 관리하는 방식도 고려할 수 있습니다. 이벤트 소싱, CQRS 등의 패턴을 활용할 수 있습니다.

 

2PC의 예시와 코드

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class TwoPhaseCommitExample {

    private static final String DB_URL = "jdbc:mysql://localhost:3306/example";
    private static final String DB_USER = "username";
    private static final String DB_PASSWORD = "password";

    public static void main(String[] args) {
        try {
            // Step 1: Establish connection with the coordinator
            Connection coordinatorConnection = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
            coordinatorConnection.setAutoCommit(false);

            // Step 2: Participants (Services) prepare to commit
            Connection service1Connection = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
            Connection service2Connection = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);

            service1Connection.setAutoCommit(false);
            service2Connection.setAutoCommit(false);

            try {
                // Perform operations in Service 1
                executeOperationInService(service1Connection, "UPDATE table1 SET column1 = value1 WHERE id = 1");

                // Perform operations in Service 2
                executeOperationInService(service2Connection, "UPDATE table2 SET column2 = value2 WHERE id = 2");

                // Step 3: Coordinator asks participants if they are ready to commit
                boolean service1Ready = askParticipantIfReady(coordinatorConnection, "Service1");
                boolean service2Ready = askParticipantIfReady(coordinatorConnection, "Service2");

                // Step 4: Commit or Rollback based on participants' responses
                if (service1Ready && service2Ready) {
                    coordinatorConnection.commit();
                    service1Connection.commit();
                    service2Connection.commit();
                    System.out.println("Transaction committed successfully.");
                } else {
                    coordinatorConnection.rollback();
                    service1Connection.rollback();
                    service2Connection.rollback();
                    System.out.println("Transaction rolled back.");
                }
            } catch (SQLException e) {
                coordinatorConnection.rollback();
                service1Connection.rollback();
                service2Connection.rollback();
                e.printStackTrace();
            } finally {
                coordinatorConnection.close();
                service1Connection.close();
                service2Connection.close();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    private static void executeOperationInService(Connection connection, String query) throws SQLException {
        PreparedStatement preparedStatement = connection.prepareStatement(query);
        preparedStatement.executeUpdate();
    }

    private static boolean askParticipantIfReady(Connection connection, String serviceName) throws SQLException {
        // Simulate the coordinator asking the participant if it's ready to commit
        // In a real-world scenario, this communication might happen over a network protocol.
        System.out.println(serviceName + ", are you ready to commit? (yes/no)");
        // Assume the participant responds with "yes" for simplicity.
        return true;
    }
}

 

이 코드는 두 개의 서비스 (Service1, Service2)가 트랜잭션을 수행하고, 코디네이터가 각 서비스에게 커밋할지 롤백할지를 묻는 2PC의 간단한 시나리오를 시뮬레이션 합니다.

'인프라 > MSA' 카테고리의 다른 글

[MSA] 분산 트랜잭션 - 이벤트 소싱  (0) 2023.12.20
[MSA] 분산 트랜잭션 - 보상 트랜잭션  (0) 2023.12.19
[MSA] 분산 트랜잭션 - ACID  (1) 2023.12.18
[MSA] Hexagonal Architecture  (0) 2023.12.07
[MSA] MSA가 어려운 이유  (0) 2023.12.01