InnoDB MVCC 트랜잭션 처리의 모든 것: Undo Log · Redo Log · Isolation Level · Undo Chain
대상: MySQL InnoDB 기준 (MVCC 개념은 여러 RDB에 공통적이지만, 구현 디테일은 엔진별로 다릅니다)
0. 한 줄 요약 (TL;DR)
- MVCC는 “락을 덜 걸고도 읽기 일관성(consistent read)을 보장”하기 위해, 한 Row의 과거 버전을 Undo Log로 이어 붙인 Undo Chain을 활용한다.
- Undo Log는 “롤백(rollback)”과 “과거 버전 제공(MVCC)”을 담당한다.
- Redo Log는 “크래시 복구(crash recovery)”를 위한 WAL(Write-Ahead Logging) 로, 커밋 시점의 내구성(durability)의 핵심이다.
- Isolation Level은 “어떤 시점의 스냅샷(Read View)을 언제 만들고, 어떤 락을 어떻게 쓰는지”의 정책이다.
- 트랜잭션의 실제 처리 흐름은 거의 항상 다음 조합으로 움직인다:
UPDATE/DELETE/INSERT 시:
(Undo 기록) → (데이터 페이지 변경) → (Redo 기록) → (락/트랜잭션 상태 관리) → commit/rollback
읽기(consistent read) 시:
Read View 생성 → (Row의 trx_id / roll_ptr 기반으로 보이는 버전 탐색)
1. 핵심 개념 지도
1.1 MVCC가 해결하려는 문제
동시성 환경에서 “읽기”와 “쓰기”가 충돌하면 전통적으로는 락을 많이 써야 해서 성능이 떨어진다.
- 읽는 동안 쓰기를 막으면: 쓰기 지연
- 쓰는 동안 읽기를 막으면: 읽기 지연
- 둘 다 빠르게 하려면: 읽기는 스냅샷을 보고, 쓰기는 최신을 쓰게 해야 한다.
그래서 InnoDB는:
- 읽기(일반 SELECT) 는 락 없이 “스냅샷”을 보도록 하고,
- 쓰기(UPDATE/DELETE/INSERT) 는 락을 걸어 정합성을 지키되,
- 읽기에게는 Undo Chain으로 “과거 버전”을 제공한다.
2. Row 버전 정보: DB_TRX_ID / DB_ROLL_PTR (InnoDB의 MVCC 포인터)
InnoDB의 각 Row(정확히는 clustered index record)는 내부적으로 다음 “숨은 컬럼”을 가진다:
DB_TRX_ID: 이 Row를 마지막으로 변경한 트랜잭션 IDDB_ROLL_PTR: 이전 버전(Undo record)을 가리키는 포인터 → Undo Chain의 연결 고리
즉, 현재 Row는 최신 버전이고, 이전 버전들은 Undo Log에 연결 리스트처럼 매달려 있다.
3. Undo Log: 롤백 + MVCC(과거 버전) 제공
3.1 Undo Log의 역할 2가지
- Rollback
- 트랜잭션이 실패/명시적 ROLLBACK 시 변경 내용을 되돌린다.
- Consistent Read (MVCC)
- 다른 트랜잭션이 “과거 스냅샷”을 읽어야 할 때, Undo에서 과거 이미지를 복원한다.
3.2 Undo는 “이전 이미지(또는 역연산 정보)”를 가진다
- UPDATE: 이전 값(before image) 또는 복원에 필요한 정보
- DELETE: “delete-mark” 이전 상태로 되돌릴 정보
- INSERT: 롤백 시 “삽입된 레코드를 제거”하기 위한 정보
중요한 포인트: InnoDB의 DELETE는 즉시 물리 삭제가 아니라 보통 delete-mark 후, 나중에 purge가 물리 제거한다.
4. Redo Log: WAL + 크래시 복구(내구성)
4.1 Redo Log의 목적
Redo는 “커밋된 변경을 디스크 데이터 파일에 반영하기 전에 장애가 나도 복구할 수 있게” 해준다.
- 데이터 페이지는 메모리(Buffer Pool)에서 먼저 바뀌고 → 더티 페이지(dirty page)가 된다.
- 더티 페이지를 디스크에 쓰는 타이밍은 나중(체크포인트/플러시)일 수 있다.
- 그런데 커밋은 “지금 성공했다”고 말해야 한다.
- 그래서 커밋 전에 Redo(Log File)만 디스크에 안전하게 기록하면,
- 장애 후 재시작 시 Redo를 다시 적용해서 커밋 결과를 복원할 수 있다.
4.2 Redo와 Undo의 관계 (크래시 복구 관점)
- Redo: “커밋된 것”을 재적용(roll forward)
- Undo: “커밋되지 않은 것”을 되돌림(roll back)
장애 복구는 큰 흐름이:
- Redo로 “커밋된 변경”을 재구성
- Undo로 “미커밋 변경”을 제거
5. 실제로 UPDATE 한 번 하면 내부에서 무슨 일이 일어나나?
예: UPDATE account SET balance = balance - 100 WHERE id=1;
5.1 단계별 처리(개념적으로)
diagram rendering...
5.2 여기서 “Undo Chain”은 언제 생기나?
- UPDATE가 발생하는 순간, 기존 Row가 바로 사라지는 게 아니라:
- Undo에 이전 버전이 기록되고
- 현재 Row의
DB_ROLL_PTR가 그 Undo를 가리키며 - 그 Undo record는 또 그 이전 Undo를 가리키는 식으로 이어짐
즉, UPDATE가 반복될수록 Row의 과거 버전 체인이 길어진다.
6. Read View: “내가 볼 수 있는 버전”을 결정하는 스냅샷
일반 SELECT(락 없는 읽기)는 “현재 최신 값”이 아니라 “스냅샷 시점에 보이는 값”을 본다.
이 스냅샷이 Read View.
6.1 Read View가 담는 핵심 정보(개념)
- “이 Read View 시점에 이미 커밋된 트랜잭션은 누구인가?”
- “아직 진행 중(active)인 트랜잭션은 누구인가?”
구현 디테일은 엔진마다 다르지만, InnoDB 개념적으로는:
- 특정 경계값(낮은 trx id, 높은 trx id)
- 활성 트랜잭션 목록 등을 통해 “이 Row를 만든 trx_id가 내 스냅샷에서 보이는가?”를 판단한다.
6.2 Consistent Read 알고리즘(개념 의사코드)
function visible(row, read_view):
if row.DB_TRX_ID == my_trx_id:
return true // 내가 만든 변경은 보임(자기 변경 읽기)
if row.DB_TRX_ID is committed before read_view:
return true
if row.DB_TRX_ID is active in read_view:
return false
else:
// 애매한 경우는 규칙에 따라 판단(경계값 기반)
6.3 안 보이면? → Undo Chain을 따라 과거 버전 탐색
- 현재 Row가 “내 스냅샷에서 안 보인다”면,
DB_ROLL_PTR로 Undo record를 찾아가서- 그 Undo로 “과거 버전”을 복원한 뒤 다시 visible 체크
- 보일 때까지 반복(또는 체인이 끝나면 없음)
7. Isolation Level: Read View 생성 타이밍 + 락 정책의 차이
InnoDB에서 MVCC 관점으로 가장 중요한 차이는 아래 2개:
- Read View를 언제 만들고 재사용하나?
- 팬텀(phantom)을 막기 위해 어떤 락을 어느 쿼리에 쓰나?
7.1 READ UNCOMMITTED (RU)
- dirty read 가능(미커밋 데이터도 읽을 수 있음)
- 실무에서는 거의 비권장(데이터 일관성 깨짐)
7.2 READ COMMITTED (RC)
- 문장(statement) 단위로 Read View 생성 → 같은 트랜잭션 안에서도 SELECT를 다시 하면 결과가 바뀔 수 있음(non-repeatable read 가능)
- 일반적으로:
- 동시성 좋고(락 적음),
- “트랜잭션 내 반복 읽기 일관성”은 약하다.
7.3 REPEATABLE READ (RR) — InnoDB 기본
- 트랜잭션(transaction) 단위로 Read View 생성(첫 consistent read 때 만들어지고 재사용) → 같은 트랜잭션 안의 일반 SELECT는 “처음 스냅샷”을 계속 본다(repeatable read 보장)
- 팬텀 방지(특히 locking read에서) 위해 next-key lock(레코드+갭) 등을 적극 사용
주의: “팬텀”은 일반 SELECT(consistent read) 만으로는 “스냅샷이 고정이라 결과가 변하지 않는 것처럼 보일 수” 있지만,
SELECT ... FOR UPDATE 같은 locking read에서는 실제로 팬텀을 막기 위한 락이 중요해진다.
7.4 SERIALIZABLE
- 가장 강한 격리
- 일반 SELECT도 읽기 락(또는 범위 락)을 더 강하게 취급하는 방향
- 동시성 비용이 크므로 특별한 경우에만 사용
8. “Consistent Read” vs “Current Read(락킹 리드)”
8.1 Consistent Read (일반 SELECT)
SELECT ...(평범한 조회)- 기본적으로 락 없이 실행
- Read View 기반으로 스냅샷을 보고, 필요하면 Undo Chain으로 과거 버전을 복원
8.2 Current Read (Locking Read)
SELECT ... FOR UPDATESELECT ... LOCK IN SHARE MODE(또는 엔진/버전에 따른 유사 문법)- “스냅샷”이 아니라 현재 최신 버전(현재 시점의 committed/locking 규칙) 을 대상으로 락을 획득하며 읽는다.
- 이때 범위 조건이 있으면 갭락/넥스트키락으로 팬텀을 막는다.
9. Undo Log Chain(History)과 Purge: 왜 “긴 트랜잭션”이 위험한가?
9.1 Undo Chain이 길어지는 이유
- UPDATE/DELETE가 반복되면 Row마다 과거 버전이 Undo에 계속 쌓인다.
- 그런데 이 과거 버전은 아무도 참조하지 않을 때 제거할 수 있다.
9.2 Purge 스레드의 역할
- “더 이상 어떤 Read View에서도 필요 없는 Undo(과거 버전)”를 정리한다.
- 즉,
- 모든 활성 트랜잭션의 스냅샷이 그 Undo보다 “미래”에 있으면,
- 그 Undo는 안전하게 삭제(purge) 가능.
9.3 긴 트랜잭션이 Purge를 막는다
- 오래 열린 트랜잭션(특히 consistent read를 한 뒤 오래 유지)은
- “아직 그 과거 버전을 볼 수도 있는 사용자”가 존재하게 만든다.
- 그 결과:
- Undo가 purge되지 못하고 계속 누적 → Undo tablespace 증가
- undo chain 길어짐 → MVCC 조회 비용 증가(더 많이 따라가야 함)
- 히스토리 리스트가 커지면 전반적인 성능/디스크 압박 발생
실무 체크리스트
- 애플리케이션에서 “읽기 전용 트랜잭션”을 오래 열어두지 않기
- 배치/리포트 쿼리는 가능한 짧게, 또는 별도 전략(스냅샷/리플리카) 고려
- 모니터링:
innodb_history_list_length(개념적으로 “undo backlog” 신호)
10. Commit / Rollback / Crash Recovery를 한 장으로
10.1 Commit 시나리오(정상)
- 트랜잭션이 변경 작업 수행
- Undo 기록(롤백+MVCC 용)
- 데이터 페이지 변경(메모리)
- Redo 기록(WAL)
- COMMIT
- Redo를 디스크에 안전하게 기록(정책에 따라 flush/fsync)
- 트랜잭션 상태 committed
- 나중에
- 더티 페이지가 디스크에 반영(체크포인트 등)
- purge가 불필요해진 undo를 제거
10.2 Rollback 시나리오
- Undo를 따라가며 변경을 되돌린다.
- (논리적으로) “변경 작업의 역연산”을 수행해 원상복구
10.3 Crash Recovery(장애 후 재시작)
- 원칙: 데이터 파일은 커밋 타이밍과 동기화되어 있지 않을 수 있다.
- 따라서:
- Redo를 적용해 “커밋된 변경”을 복원
- Undo로 “미커밋 변경”을 제거
11. 예제로 보는 버전 가시성(Undo Chain 따라가기)
가정: books(id=1, title='A')
타임라인
- Tx1: REPEATABLE READ에서 조회 후 오래 유지
- Tx2: title을 여러 번 수정하고 커밋
t0: Tx1 START
t1: Tx1 SELECT title FROM books WHERE id=1; // Read View 생성(스냅샷 고정)
-> 'A'를 봄
t2: Tx2 START
t3: Tx2 UPDATE books SET title='B' WHERE id=1; COMMIT
t4: Tx2 UPDATE books SET title='C' WHERE id=1; COMMIT
- 현재 Row는 'C'지만,
- Tx1의 스냅샷에서는 Tx2의 trx_id가 “스냅샷 이후”면 보이면 안 됨
- 그래서 Tx1은:
- 현재 Row('C')는 invisible → roll_ptr로 Undo('B') 확인
- 'B'도 invisible → roll_ptr로 Undo('A') 확인
- 'A'는 visible → 결과는 'A'
즉, Tx1이 오래 살아 있으면 'A' 버전을 purge 못 하고 계속 유지해야 한다.
12. 실무에서 “격리수준 + 쿼리 패턴” 선택 가이드
- 트랜잭션 내 동일 SELECT 결과가 반드시 같아야 하나?
- Yes → RR 고려
- No → RC로 동시성/락 경합 완화 가능
- 범위 업데이트/락킹 리드가 잦은가?
- RR에서 next-key lock로 경합이 커질 수 있음 → 인덱스/쿼리 조건 점검
- 장시간 트랜잭션이 존재하는가?
- Undo 증가/성능 저하의 가장 흔한 원인 → 트랜잭션 경계/커넥션 관리 최우선
13. 요약: “트랜잭션이 처리되는 진짜 메커니즘”
-
쓰기(UPDATE/DELETE/INSERT) 는:
- 정합성을 위해 락을 쓰고,
- Undo로 과거를 남기고,
- Redo로 커밋 내구성을 보장하고,
- 데이터 파일 반영은 “나중”일 수 있다.
-
읽기(일반 SELECT) 는:
- Read View(스냅샷)으로 일관성을 만들고,
- 최신이 안 보이면 Undo Chain으로 과거 버전을 복원한다.
-
Isolation Level 은:
- Read View 생성/재사용 타이밍과,
- locking read에서 팬텀을 막는 락 전략을 결정한다.
Appendix A. 용어 빠른 사전
- MVCC: Multi-Version Concurrency Control (다중 버전 동시성 제어)
- Undo Log: 롤백/과거버전 제공 로그
- Undo Chain: row의 roll_ptr가 가리키는 과거 버전 연결 구조
- Redo Log: WAL, 크래시 복구용 로그
- Read View: consistent read를 위한 스냅샷 메타데이터
- Consistent Read: 스냅샷 기반 읽기(락 최소)
- Current Read: 최신 버전 + 락 획득 읽기(FOR UPDATE 등)
- Purge: 더 이상 필요 없는 undo/삭제 마크 레코드 정리
Appendix B. “왜 Undo와 Redo를 둘 다 쓰나?”
- Redo만 있으면 롤백/스냅샷 읽기(MVCC)가 어렵다.
- Undo만 있으면 크래시 후 “커밋 결과”를 안정적으로 복원하기 어렵다.
- 그래서 InnoDB는 Undo(논리적 되돌림/과거버전) + Redo(내구성/복구) 를 조합한다.