-
Notifications
You must be signed in to change notification settings - Fork 0
Open
Labels
Description
📝 선착순 시스템에서 데이터 무결성을 어떻게 보장할까?
📚 주제:
여러 사용자가 동시에 빠른 접근이 이루어지는 선착순 시스템에서 정확한 데이터를 읽기 위한 방법
🎯 스터디 목표
선착순 시스템에서의 데이터 락
트랜잭션의 격리 레벨 (Transaction Isolation Level)
트랜잭션의 격리 레벨(Isolation Level) 은 동시에 여러 트랜잭션이 실행될 때, 서로의 작업이 얼마나 영향을 미칠 수 있는지를 결정하는 설정입니다. 격리 레벨이 높을수록 데이터 정합성이 보장되지만, 동시성이 낮아지고 성능 저하가 발생할 수 있습니다. ANSI SQL 표준에서 정의한 4가지 격리 수준과 발생할 수 있는 문제점은 다음과 같습니다.
1. READ UNCOMMITTED (읽기 미확정)
- 트랜잭션이 커밋되지 않은 데이터도 읽을 수 있는 가장 낮은 격리 수준
- 성능은 가장 좋지만, Dirty Read(더티 리드) 발생 가능
발생할 수 있는 문제
- Dirty Read(더티 리드): 다른 트랜잭션에서 변경했지만 아직 커밋되지 않은 데이터를 읽을 수 있음 → 이후 롤백되면 잘못된 데이터를 읽은 셈이 됨
2. READ COMMITTED (읽기 확정)
- 커밋된 데이터만 읽을 수 있는 수준
- 대부분의 RDBMS에서 기본 설정 (ex. Oracle, PostgreSQL)
- Dirty Read 방지 가능하지만, Non-Repeatable Read 발생 가능
발생할 수 있는 문제
- Non-Repeatable Read(비반복 읽기): 같은 트랜잭션 내에서 같은 데이터를 두 번 읽었을 때 값이 다를 수 있음 → 다른 트랜잭션이 데이터를 변경하고 커밋했기 때문
3. REPEATABLE READ (반복 가능한 읽기)
- 같은 트랜잭션 내에서는 읽은 데이터가 변하지 않도록 보장 (즉, 트랜잭션이 끝날 때까지 다른 트랜잭션이 해당 데이터를 수정할 수 없음)
- MySQL의 기본 격리 수준
- Dirty Read, Non-Repeatable Read 방지 가능하지만, Phantom Read 발생 가능
발생할 수 있는 문제
- Phantom Read(팬텀 리드): 같은 조건으로 데이터를 조회했을 때, 새로운 행이 추가되거나 삭제될 수 있음 → 다른 트랜잭션이 데이터를 삽입하면 조회 결과가 달라짐
4. SERIALIZABLE (직렬화)
- 가장 높은 격리 수준으로, 트랜잭션을 직렬적으로 실행하는 것과 같은 효과를 가짐 (Locking을 사용하여 동시 실행 방지)
- 모든 문제(Dirty Read, Non-Repeatable Read, Phantom Read)를 해결하지만 성능 저하가 심함
READ COMMITTED + 행 락(FOR UPDATE)을 활용한 선착순 시스템 구축
1. READ COMMITTED + FOR UPDATE를 사용하는 이유
- Dirty Read 방지: 커밋된 데이터만 읽을 수 있음
- 선착순 보장: 같은 데이터를 여러 사용자가 동시에 수정하는 것을 방지
- 트랜잭션 내에서 데이터 일관성 유지: 하나의 트랜잭션이 데이터를 수정 중이면 다른 트랜잭션은 대기
2. 선착순 시스템에서의 적용 예시
✅ 좌석 예약 시스템 (Seat Booking)
BEGIN;
-- 특정 좌석을 조회하면서 해당 좌석에 대한 락 설정
SELECT * FROM seats WHERE seat_id = 1 FOR UPDATE;
-- 좌석이 예약되지 않았다면, 예약 진행
UPDATE seats SET status = 'RESERVED' WHERE seat_id = 1;
COMMIT;📌 처리 흐름
seat_id = 1인 좌석을 조회하면서 행 락 설정- 다른 사용자가 같은 좌석을 예약하려고 하면 대기 상태가 됨
UPDATE실행 후COMMIT→ 락 해제
✅ 이 방식의 장점
- 같은 좌석에 대한 중복 예약 방지
- 여러 사용자가 동시에 요청해도 첫 번째 트랜잭션이 완료될 때까지 다른 요청은 대기
🚨 주의점
- 트랜잭션이 길어지면 성능 저하 발생 → 최대한 빠르게 커밋해야 함
- 대기하는 트랜잭션이 많으면 타임아웃 문제 발생 가능 → 이를 방지하려면
NOWAIT또는SKIP LOCKED옵션을 사용할 수 있음
3. 성능 최적화를 위한 추가 옵션
✅ NOWAIT: 즉시 실패하도록 설정
SELECT * FROM seats WHERE seat_id = 1 FOR UPDATE NOWAIT;🚨 즉시 실패 (대기 없음)
- 락이 걸려 있다면 즉시 오류 발생
- 대기하지 않고 다른 처리를 할 수 있음
✅ SKIP LOCKED: 사용 가능한 데이터만 조회
SELECT * FROM seats WHERE status = 'AVAILABLE' FOR UPDATE SKIP LOCKED LIMIT 1;✅ 장점
- 대기 시간을 줄여 빠른 응답 가능
- 동시에 많은 사용자가 몰리는 선착순 시스템에서 유용
🚨 주의점
- 특정 요청이 계속해서 락이 걸려 있는 데이터를 건너뛸 가능성이 있음
- 공정성(Fairness)이 부족할 수 있음
4. 결론
✅ READ COMMITTED + 행 락(FOR UPDATE)이 적절한 경우
- 좌석 예약, 한정 수량 쿠폰 발급, 재고 감소 등의 선착순 시스템
- 동시에 여러 사용자가 같은 데이터를 수정하는 경우
✅ 효율적인 트랜잭션 관리 방법
FOR UPDATE를 사용하여 중복 요청 방지NOWAIT을 활용해 즉시 실패하도록 설정 (불필요한 대기 방지)SKIP LOCKED를 사용하여 빠른 처리 가능 (대량 트래픽 대응)
💡 결론적으로, READ COMMITTED + FOR UPDATE가 선착순 시스템에서는 가장 현실적인 조합!
하지만 상황에 따라 SKIP LOCKED 등의 추가 옵션을 활용하면 성능을 더욱 향상시킬 수 있습니다. 🚀
Kafka를 활용한 선착순 시스템 구축 및 부하 분산 방법
1. Kafka를 활용한 순차적 선착순 처리
- 사용자가 요청을 보냄 → Kafka의 특정 Topic에 메시지 전송
- Kafka Producer가 메시지를 저장 → 메시지 큐에 보관
- Kafka Consumer가 하나씩 메시지를 가져와 순차적으로 처리
Kafka Producer 예제
KafkaTemplate<String, String> kafkaTemplate;
public void sendReservationRequest(String userId) {
kafkaTemplate.send("reservation_topic", userId);
}Kafka Consumer 예제
@KafkaListener(topics = "reservation_topic", groupId = "reservation_group")
public void processReservation(String userId) {
System.out.println("Processing reservation for user: " + userId);
}2. Kafka 부하 분산 방법
✅ 다중 Consumer를 활용한 병렬 처리
- 파티션을 늘려 Consumer Group을 병렬 실행
- Consumer 인스턴스를 Auto Scaling하여 부하를 동적으로 조절
✅ Retention 설정 조정
log.retention.hours=6 # 메시지를 6시간 동안만 유지✅ Batch 처리 방식 적용
@KafkaListener(topics = "reservation_topic", containerFactory = "batchFactory")
public void processReservation(List<String> messages) {
for (String message : messages) {
System.out.println("Processing batch message: " + message);
}
}결론
Kafka를 활용하면 비동기적 선착순 처리가 가능하며, 다음 방법을 통해 부하를 효율적으로 분산할 수 있음:
- 파티션을 늘려 Consumer를 병렬 처리
- Auto Scaling을 활용한 동적 확장
- Retention 설정을 최적화하여 불필요한 데이터 삭제
- Batch 처리 방식을 적용하여 성능 최적화
💡 참고 자료:
- 링크나 참고한 자료 출처를 여기에 적어 주세요.