반응형
2024.11.06 - [개발/sql] - 2 Phase Lock & mysql
2024.02.29 - [architecture/micro service] - [arch] EDA, event sourcing, saga, 2pc
2PC, 2PL 모두 알아봤는데 뭔가 미래에도 헷갈릴 것 같아서 정리...
1. 2PC (Two-Phase Commit)
2PC는 분산 트랜잭션 관리 방식으로, 여러 시스템에서 분산된 트랜잭션을 일관되게 관리하고 커밋/롤백을 보장하는 프로토콜
2PC는 MSA의 서로 다른 서비스 간(회원 컴포넌트 & 배송 컴포넌트), 즉 분리된 데이터의 저장에 대해서(하나의 동작이지만) 하나의 트랜젝션으로 묶지 못하기 때문에 데이터가 틀어지지 않게 하는 방법이다
2PC는 현재 잘 안 쓰이고 MSA의 경우 결국 SAGA chreography가 더 자주 쓰이는 듯. 비동기에 이벤트 기반인 게 제일 안전하다. 결국 메시지 브로커를 누가 누가 잘 쓰냐의 경쟁이랄까..
동작 방식:
- Phase 1: Prepare Phase
- 트랜잭션 코디네이터가 참가자들에게 트랜잭션을 커밋할 준비가 되었는지 물어봅니다 (Vote).
- 각 참가자는 트랜잭션이 커밋 가능한지, 아니면 롤백해야 하는지를 답변합니다.
- 모든 참가자가 "Yes"를 응답하면 커밋을 진행하고, 하나라도 "No"를 응답하면 롤백합니다.
- ex. 주문 서비스(코디네이터)는 각 서비스(결제, 재고, 배송 등)에게 트랜잭션을 준비할 수 있는지 확인합니다. 각 서비스는 작업을 완료할 준비가 되었는지 확인하고, 준비되면 "Yes"를 응답합니다. 각 서비스는 해당 작업을 실제로 실행하지 않고, 작업을 예약해 놓습니다.
- 트랜젝션 걸고 락 필요
- Phase 2: Commit/Rollback Phase
- 트랜잭션 코디네이터는 모든 참가자가 "Yes"라고 응답하면 커밋을 확정하고, 하나라도 "No"라고 응답하면 롤백합니다.
- 트랜잭션 코디네이터는 모든 참가자들에게 최종 결과를 전달합니다.
- ex. 모든 서비스가 "Yes" 응답을 하면, 주문 서비스는 모든 서비스에 대해 트랜잭션을 커밋하도록 지시합니다. 하나라도 "No" 응답이 있으면, 주문 서비스는 모든 서비스에 롤백을 지시합니다.
import javax.transaction.TransactionManager;
import javax.transaction.UserTransaction;
import javax.naming.InitialContext;
public class TwoPhaseCommitExample {
public static void main(String[] args) {
try {
// JNDI를 통해 트랜잭션 관리자를 조회
InitialContext ctx = new InitialContext();
UserTransaction utx = (UserTransaction) ctx.lookup("java:comp/UserTransaction");
// 1단계: 트랜잭션 시작
utx.begin();
System.out.println("Transaction started");
// 참여자 1: 데이터베이스 A
DatabaseParticipant dbA = new DatabaseParticipant("DB_A");
dbA.prepare();
// 참여자 2: 데이터베이스 B
DatabaseParticipant dbB = new DatabaseParticipant("DB_B");
dbB.prepare();
// 2단계: 커밋 요청
dbA.commit();
dbB.commit();
System.out.println("Transaction committed successfully");
// 트랜잭션 종료
utx.commit();
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 참여자 역할을 하는 클래스
class DatabaseParticipant {
private String name;
public DatabaseParticipant(String name) {
this.name = name;
}
// 준비 단계
public void prepare() {
try {
connection.setAutoCommit(false); // 트랜잭션 시작
// 락 잡기
PreparedStatement stmt = connection.prepareStatement("SELECT * FROM accounts WHERE id = ? FOR UPDATE");
stmt.setInt(1, 1);
stmt.executeQuery();
// 데이터 수정
connection.prepareStatement("UPDATE accounts SET balance = balance - 100 WHERE id = 1").executeUpdate();
System.out.println(name + " is prepared for the transaction");
} catch (SQLException e) {
e.printStackTrace();
rollback();
}
}
// 커밋 단계
public void commit() {
System.out.println(name + " has committed the transaction");
}
// 롤백 단계
public void rollback() {
System.out.println(name + " has rolled back the transaction");
}
}
장점:
- 원자성 보장: 모든 참가자가 트랜잭션을 커밋하거나 모두 롤백하여 트랜잭션의 원자성을 보장합니다.
- 분산 트랜잭션 처리: 여러 시스템에서 하나의 트랜잭션을 관리할 수 있습니다.
단점:
- Blocking 문제: 트랜잭션이 실패하거나 코디네이터가 실패하면, 참가자는 "결정할 수 없다"는 상태로 대기 상태에 빠질 수 있습니다. 트랜잭션 코디네이터나 참가자 서비스가 다운되면 트랜잭션이 중단될 수 있습니다.
- 성능 저하: 2PC는 동기식으로 트랜잭션을 처리하기 때문에, 각 서비스 간의 통신과 협의 과정에서 지연이 발생할 수 있습니다.
- 복잡성: 2PC는 구현이 복잡하며, 특히 장애 복구와 같은 시나리오에서 상태를 관리하는 데 어려움이 있을 수 있습니다.
- 데드락: 준비 단계에서 여러 트랜잭션이 교착 상태에 빠질 수 있으므로, 트랜잭션 순서 및 타임아웃을 적절히 설정해야 합니다. prepare 했는데 commit 못하고 죽으면,, 영원한 데드락?(타임아웃 설정 필요)
- 락 경합: 준비 단계에서 락이 너무 오래 유지되면, 다른 트랜잭션이 대기 상태가 되어 성능 저하가 발생할 수 있습니다. 이를 해결하려면 트랜잭션 범위를 최소화하고, 락 해제를 신속히 수행해야 합니다.
2. 2PL (Two-Phase Locking)
2LC는 데이터베이스의 동시성 제어 방법으로, 트랜잭션이 데이터에 대해 잠금을 설정하고 해제하는 순서를 규정하는 방식
디비(mysql) 내부에서 같은 요청이 여러번 들어왔을 때, 어떻게 잠그냐~ 하는 방법
@Transactional Isolation과 연관하여
= 데이터 일관성과 성능 간의 균형을 맞추는 방법, 기본적으로데이터베이스의 격리 수준을 기반으로 동작
1. READ_COMMITTED (기본)
- 매핑된 2PL 방식: Basic 2PL
- 트랜잭션이 진행되면서 필요한 시점에만 락을 걸고, 트랜잭션이 종료될 때 락을 해제
- Dirty Read, Non-Repeatable Read 방지
- Phantom Read가 발생할 수 있으며, 이 경우 Repeatable Read나 Serializable 격리 수준을 사용해야 함
2. READ_UNCOMMITTED
- 매핑된 2PL 방식: None (락을 관리하지 않음)
- 트랜잭션 간에 락을 걸지 않으며, Dirty Read가 가능하고 동시성 제어가 거의 없음
- 2PL 방식이 적용되지 않음
3. REPEATABLE_READ(mySql default)
- 매핑된 2PL 방식: S2PL (Strict 2PL)
- 트랜잭션이 데이터를 읽고 쓰는 동안 해당 데이터를 락을 걸고 트랜잭션 종료 시점에 락을 해제
- Phantom Read, Non-Repeatable Read와 Dirty Read 방지
4. SERIALIZABLE
- 매핑된 2PL 방식: SS2PL (Strong Strict 2PL)
- 트랜잭션이 시작될 때 모든 락을 미리 걸고, 트랜잭션 종료 시점에 락을 해제
- Phantom Read, Non-Repeatable Read, Dirty Read를 방지하고, 모든 트랜잭션 간의 충돌을 방지
- 트랜잭션이 끝날 때까지 모든 락을 보유하므로 성능이 안 좋음
동작 방식:
- Phase 1: Growing Phase
- 트랜잭션은 필요한 잠금을 획득할 수 있으며, 이 시점에서만 데이터에 대한 잠금을 증가시킬 수 있습니다. 새로운 잠금을 얻거나 기존 잠금을 확장할 수 있습니다.
- Phase 2: Shrinking Phase
- 트랜잭션은 잠금을 더 이상 추가할 수 없으며, 잠금을 해제하는 시점입니다. 잠금 해제는 commit 또는 rollback 후에 이루어집니다.
장점:
- Serializable 격리 수준 보장: 이 방식은 데이터의 일관성을 유지하면서, 트랜잭션 간의 충돌을 방지할 수 있습니다.
- 동시성 제어: 두 개의 단계에서 데이터 잠금과 해제를 관리하여 동시성 문제를 해결합니다.
단점:
- 교착 상태 (Deadlock): 만약 여러 트랜잭션이 서로의 잠금을 기다리게 되면 교착 상태가 발생할 수 있습니다. 이를 해결하려면 교착 상태 감지 및 회피 기법이 필요합니다.
- 성능 저하: 잠금 관리가 복잡하여 성능에 부정적인 영향을 미칠 수 있습니다.
728x90
반응형
'architecture > micro service' 카테고리의 다른 글
대용량 데이터 처리 고민 (1) | 2024.11.10 |
---|---|
transaction outbox pattern + polling publisher pattern (0) | 2024.11.07 |
[arch] EDA, event sourcing, saga, 2pc (0) | 2024.02.29 |
[inflearn] Spring Cloud로 개발하는 마이크로서비스 애플리케이션(MSA) (0) | 2024.02.18 |
[gRPC] gRPC gateway란 (0) | 2022.01.24 |