트랜잭션 격리 수준이 올라감에 따라 동시성 문제를 해결하는 방법

격리 수준이 올라갈수록 MVCC 스냅샷의 유지 범위가 넓어지고, 락의 범위와 강도가 강화됨으로써 더 많은 이상 현상을 방지합니다.

전체 구조

READ UNCOMMITTED → READ COMMITTED → REPEATABLE READ → SERIALIZABLE
    (제한 없음)      (MVCC 추가)      (스냅샷 고정)      (락 강화)

격리 수준이 올라갈수록:
1. MVCC 스냅샷 유지 기간이 길어지고
2. 락의 범위가 넓어지고
3. 락의 종류가 추가됨

단계별 방지 메커니즘 (InnoDB 기준)

1단계: READ UNCOMMITTED → READ COMMITTED

추가되는 메커니즘: MVCC 스냅샷 도입

[READ UNCOMMITTED — 아무런 보호 없음]
트랜잭션A: UPDATE price = 50000 (미커밋)
트랜잭션B: SELECT price → 50000 읽음 ← Dirty Read X

→ 현재 데이터를 그대로 읽음
→ 커밋 여부와 관계없이 최신 값을 보여줌

[READ COMMITTED — MVCC 스냅샷 도입]
트랜잭션A: UPDATE price = 50000 (미커밋)
트랜잭션B: SELECT price → ?

InnoDB 내부:
  테이블: price = 50000 (트랜잭션A가 수정, 미커밋)
  Undo Log: price = 45000 (이전 버전)

  트랜잭션B: "A는 아직 커밋 안 됐으니 Undo Log에서 읽자"
           → price = 45000 O Dirty Read 방지

→ 매 SELECT마다 "현재 커밋된 데이터"의 스냅샷을 생성
→ 커밋되지 않은 변경은 Undo Log의 이전 버전으로 우회
[방지 원리]

SELECT 실행 시:
1. 해당 행을 수정한 트랜잭션이 있는지 확인
2. 수정한 트랜잭션이 커밋되지 않았으면
3. Undo Log에서 커밋된 버전을 찾아서 반환

→ Dirty Read 방지 O
→ 단, 매번 새 스냅샷이므로 Non-Repeatable Read는 여전히 발생
[Non-Repeatable Read가 발생하는 이유]

트랜잭션B: SELECT price → 스냅샷①  → 45000
트랜잭션A: UPDATE price = 39000 + COMMIT
트랜잭션B: SELECT price → 스냅샷②  → 39000 ← 새 스냅샷에서 A의 커밋 반영

→ 스냅샷을 매번 새로 생성하므로
→ 커밋된 변경이 다음 SELECT에 반영됨

2단계: READ COMMITTED → REPEATABLE READ

추가되는 메커니즘: 스냅샷 고정 + 갭 락

[READ COMMITTED — 매번 새 스냅샷]
트랜잭션B 시작
  SELECT → 스냅샷① 생성 → 45000
  (트랜잭션A: UPDATE 39000 + COMMIT)
  SELECT → 스냅샷② 생성 → 39000 ← Non-Repeatable Read X

[REPEATABLE READ — 스냅샷 고정]
트랜잭션B 시작
  SELECT → 스냅샷① 생성 → 45000
  (트랜잭션A: UPDATE 39000 + COMMIT)
  SELECT → 스냅샷① 재사용 → 45000 O Non-Repeatable Read 방지

→ 첫 SELECT 시 스냅샷을 생성하고
→ 트랜잭션이 끝날 때까지 같은 스냅샷을 유지
→ 다른 트랜잭션의 커밋이 보이지 않음
[스냅샷 비교]

READ COMMITTED:
  SELECT①  SELECT②  SELECT③
     ↓        ↓        ↓
  스냅샷①  스냅샷②  스냅샷③  ← 매번 새로 생성

REPEATABLE READ:
  SELECT①  SELECT②  SELECT③
     ↓        ↓        ↓
  스냅샷①  스냅샷①  스냅샷①  ← 계속 재사용

갭 락 추가 (InnoDB 고유)

[잠금 읽기에서의 팬텀 방지]

REPEATABLE READ에서 SELECT ... FOR UPDATE 실행 시:

price 인덱스: [5000] ─(gap)─ [10000] ─(gap)─ [20000] ─(gap)─ [30000]

SELECT * FROM orders WHERE price > 10000 FOR UPDATE;

READ COMMITTED:
  → 10000, 20000, 30000 레코드 락만
  → (10000, 20000) 사이에 INSERT 가능 → 팬텀 발생

REPEATABLE READ (InnoDB):
  → 레코드 락 + 갭 락 = 넥스트 키 락
  → (10000, +∞) 전체에 갭 락
  → 이 범위에 INSERT 불가 → 팬텀 방지 O
READ COMMITTED의 락 범위:
[5000] [10000] [20000] [30000]
                          ← 레코드만 잠금

REPEATABLE READ의 락 범위 (InnoDB):
[5000] [10000] [20000] [30000] [+∞)
          ││  ← 레코드 + 갭 전체 잠금

3단계: REPEATABLE READ → SERIALIZABLE

추가되는 메커니즘: 모든 SELECT에 자동으로 공유 락

[REPEATABLE READ]
일반 SELECT: MVCC 스냅샷 (락 없음)
FOR UPDATE:  넥스트 키 락

→ 일반 SELECT는 락이 없으므로 쓰기와 동시 실행 가능

[SERIALIZABLE]
일반 SELECT: 자동으로 SELECT ... LOCK IN SHARE MODE로 변환
           → 공유 락(S-Lock) 획득
FOR UPDATE:  넥스트 키 락

→ 모든 읽기에 공유 락이 걸림
→ 읽기 중에 다른 트랜잭션이 수정/삽입 불가
[SERIALIZABLE의 동작]

트랜잭션A: SELECT * FROM orders WHERE price > 10000
           → 자동으로 공유 락 획득 (읽기 락)

트랜잭션B: INSERT INTO orders (price) VALUES (15000)
           → 대기 ⏳ (A의 공유 락과 충돌)

트랜잭션B: UPDATE orders SET price = 50000 WHERE id = 1
           → 대기 ⏳ (A의 공유 락과 충돌)

트랜잭션A: COMMIT (공유 락 해제)
트랜잭션B: 이제 실행

→ 읽기와 쓰기가 동시에 불가능
→ 사실상 순차 실행과 같은 결과
[REPEATABLE READ vs SERIALIZABLE]

REPEATABLE READ:
읽기 (일반 SELECT) ──→ 락 없음 (MVCC)
읽기 (FOR UPDATE)  ──→ 배타 락 + 갭 락
쓰기               ──→ 배타 락 + 갭 락

→ 일반 SELECT와 쓰기가 동시에 가능 O

SERIALIZABLE:
읽기 (모든 SELECT) ──→ 공유 락 + 갭 락 (자동)
쓰기               ──→ 배타 락 + 갭 락

→ 읽기와 쓰기가 동시에 불가능 X
→ 공유 락 vs 배타 락 충돌로 대기 발생

전체 비교 요약

격리 수준추가되는 메커니즘방지하는 현상
READ UNCOMMITTED없음없음
READ COMMITTED+ MVCC 스냅샷 (매 SELECT)+ Dirty Read
REPEATABLE READ+ 스냅샷 고정 + 갭 락 (InnoDB)+ Non-Repeatable Read (+ Phantom)
SERIALIZABLE+ 모든 SELECT에 공유 락+ Phantom Read (완전 보장)

락 범위 시각화

[READ UNCOMMITTED]
아무 락 없음, 스냅샷 없음
→ 모든 이상 현상 발생 가능

[READ COMMITTED]
MVCC: 매 SELECT마다 새 스냅샷
락:   쓰기 시 레코드 락만
[5000] [10000] [20000] [30000]
                        ← 수정하는 행만 잠금

[REPEATABLE READ — InnoDB]
MVCC: 트랜잭션 첫 SELECT 시 스냅샷 고정
락:   레코드 락 + 갭 락 (잠금 읽기/쓰기 시)
[5000] [10000] [20000] [30000] [+∞)
          ││ ← 레코드 + 갭 잠금

[SERIALIZABLE]
MVCC: 스냅샷 고정
락:   모든 SELECT에 공유 락 + 갭 락 자동 적용
[5000] [10000] [20000] [30000] [+∞)
││ ← 읽기만 해도 전체 잠금

성능 영향

READ UNCOMMITTED:
  읽기: 락 X 스냅샷 X → 가장 빠름
  쓰기: 제한 없음

READ COMMITTED:
  읽기: 락 X 스냅샷 O (매번 생성) → 빠름
  쓰기: 레코드 락만 → 동시성 높음

REPEATABLE READ:
  읽기: 락 X 스냅샷 O (고정) → 빠름
  쓰기: 레코드 락 + 갭 락 → 동시성 중간
  Undo Log: 트랜잭션 끝까지 유지 → 메모리 소비

SERIALIZABLE:
  읽기: 공유 락 O → 느림 (쓰기와 충돌)
  쓰기: 배타 락 + 갭 락 → 동시성 최저
  → 사실상 순차 실행

면접 포인트

CSLOG에서 보기