
MVCC란?
MVCC는 동시성을 높이면서 락을 최소화하기 위해 나온 기술 입니다.
즉, 여러 트랜잭션이 동시에 데이터를 읽고 쓰는데 서로 방해 없이 처리되도록 하려고 나온 개념 입니다.
MVCC가 나온 이유
왜 나왔나???

데이터베이스에서 여러 트랜잭션이 동시에 같은 데이터를 조회/수정할 때 충돌이 발생 합니다.
문제1) 쓰기 lock 때문에 읽기 작업이 막힘
누군가가 update하면 row에 lock을 걸어서 다른사람은 select를 못합니다.
읽기 처리량(read throughput)이 심각하게 떨어집니다.
데이터베이스 병목이 발생 합니다.
문제2) 동시성이 낮음
업데이트가 많은 테이블은 항상 락 경합(lock contention)때문에 대기상태가 늘어납니다.
그래서 해결책으로 MVCC가 나오게 됩니다.
이 기술의 기본 아이디어는 데이터를 여러 버전으로 관리해서 읽는 사람은 쓰는 사람때문에 기다리게 하지 말자! 입니다.
즉, select는 오래된 안정된 snapshot을 읽고 update는 새로운 버전을 만들어 서로 간섭하지 않게 합니다.
결론적으로 읽기는 절대 블로킹이 되지 않으며 쓰기는 기존 데이터를 덮어쓰지 않고 새 버전을 생성 합니다.
삭제는 삭제 마크만 남기고 즉시 삭제는 하지 않습니다.
트랜잭션 종료 후 쓸모없는 버전은 정리 합니다.
동작은?(버전 관리 방식)
- 하나의 노트(DB)에 여러 페이지라고 비유를 하면
- 같은 행(row)을 시간별로 여러 버전으로 저장해 두고 각 트랜잭션이 시작 했을 때 각자의 페이지를 생성
- 즉, 노트에 여러 페이지를 써놓을수 있음
- 1페이지 : 잔고 = 100원
- 2페이지 : 잔고 = 120원
- 3페이지 : 잔고 = 90원
- 각 트랜잭션은
- 나는 1페이지 기준으로 볼래
- 나는 2페이지 기준으로 볼래
- 트랜잭션이 시작할 때(=스냅샷)
- 트랜잭션이 시작되면 DB는 "지금 이 시점에 커밋된 데이터만 보여줄께"
- 그리고 이 시점 이후에 일어나는 일은 보일지 말지 내가 규칙에 따라 정할께"
- 이것을 스냅샷(Read View)이라고 부릅니다.
- 트랜잭션이 시작된 순간 = 내 기준 시간
- 읽을 때는(select) 어떻게 동작할까?
- 예를들어 처음 데이터: 잔고 = 100원
- 트랜잭션 T1이 시작(=스냅샷 찍음)
- 그 다음에 T2가 와서 잔고 = 200으로 update하고 commit
- T1입장에서의 select(읽기)
- T1은 이미 잔고=100원의 시점의 스냅샷을 들고 있습니다.
- DB는 row에 여러버전(100,200)을 들고 있다가 T1이 시작하기 전에 커밋된 버전 100원을 T1에게 보여줍니다.
- 결과 100원
- 새로운 트랜잭션 T3이 시작(=스냅샷 찍음)해서 읽기를 하면?
- T3은 T2가 200으로 업데이트 및 커밋한 이후에 시작했습니다.
- 그러면 DB는 T3 스냅샷 기준 최신 커밋 버전인 200원을 보여줍니다.
- 결과는 200원
- 결론적으로 데이터는 여러버전이 존재하며 각 트랜잭션마다 보여줄 버전이 다릅니다.
- 쓸 때(update)는 어떻게 동작할까?
- tip. update는 덮어쓰기가 아니라 새 페이지 추가하기!
- 예를 들어 현재 저장 된 버전에 100원이 있고
- T2가 update로 200원으로 바꾼다면
- DB 내부에서는 기존 버전을 Undo Log에 남김(예전 버전 저장)
- 새 버전(200원)을 row의 최신 값으로 기록
- "예전 버전은 여기 있어요~"라고 포인터를 걸어 놓습니다.
- 결과적으로 최신 트랜잭션은 200원을 바라봅니다.
- 예전 스냅샷을 가진 트랜잭션은 최신버전이 "너가 시작할 때 이후에 수정된거네? 하고 Undo를 따라가서 100원의 버전을 읽습니다.
- 결론적으로 한 행(row)에 버전들이 줄줄이 연결되어 있는 구조 입니다.
이러한 동작방식으로 인해 select 시 lock을 걸지 않습니다.
lock을 걸지 않는다는 것은 select 시 다른 사람의 update를 기다릴 필요가 없다는 뜻 입니다.
그냥 자기 스냅샷 기준으로 맞는 버전을 찾아서 그것을 읽으면 됩니다.
이런것을 Non-locking Consistent Read라고 부릅니다.
그럼 스냡샷이 계속 생기는데 이렇게 쌓이는 예전버전들은 어떻게하나??
계속 쌓이나? 쌓이기만 하면 disk 터짐! ㅋㅋ
그래서 DB에는 청소해주는 쓰레드(purge)가 있습니다.
JVM에서 안쓰는 객체들을 청소해주는 gc처럼 말이져!
아무 트랜잭션에서 사용하지 않는 오래된 버전은 조금씩 삭제 합니다.
요약
트랜잭션 시작 스냅샷(Read View) 하나 생성
select는 그 스냅샷 기준으로 "적절한 버전"을 골라 읽음
update,delete는 새 버전을 만들고 예전 버전은 undo log에 남겨놓음
안쓰는 옛날 버전은 나중에 청소 됨.
그런데 말입니다..

오래된 버전을 삭제를 해야하는데 말입니다.
오래 살아있는 트랜잭션이 많으면? 즉, 청소를 못 해서 undo/버전이 엄청 쌓이면?
디스크, 메모리, I/O 다 터지는거 아닌가?? 맞다! 다 터짐!
MVCC undo 폭증! = 트랜잭션을 너무 오래 잡아두는 뭔가가 있다..!
그럼 오래 사는 트랜잭션을 어떻게 처리해야하는가?
- 트랜잭션을 오래잡고 있는 쿼리를 찾아서 강제 종료 시키기!
- 긴 select 대량 작업을 read only 트랜잭션으로 만들기
@Transactional(readOnly = true)
- 불필요한 트랜잭션을 길게 잡지 말자!
ex) API에서 DB 트랜잭션 열고→ 외부 API 호출→ 파일 S3 업로드→ Kafka 메시지 대기→ 로직 계산→ 마지막에 commit
DB는 그 시간동안 스냅셧을 유지해야해서 undo log가 미친듯이 쌓임!
DB트랜잭션 안에서는 DB 관련 작업만 수행하자!
네트워크 호출, kafka 처리, 파일저장 등은 트랜잭션 밖에서 수행!
- 배치작업이 너무 크케 묶여있으면 쪼개자(chunking 조절)
100만건을 1개의 트랜잭션으로 처리하면 undo log가 많이 쌓임
1,000건씩 chunk 처리! 즉, commit 주기를 짧게 조절 합니다.
각 chunk 사이에 서버가 undo를 청소할 시간을 확보!
'역량 UP! > Architecture' 카테고리의 다른 글
| 이벤트 소싱(Event Sourcing)이란? (0) | 2025.11.18 |
|---|---|
| CQRS(=Command Query Responsibility Segregation)를 알아보자! (0) | 2025.11.14 |
| Backoff Retry와 DLQ(Dead Letter Queue) 차이? 사용법? (2) | 2025.11.11 |
| 동시성(Concurrency) 모델&스케줄링 모델(feat. ForkJoinPool) (0) | 2025.11.04 |
| Blocking&non-Blocking 그리고 Sync&Async 이해하기! (0) | 2025.11.03 |