DB 핵심 정리
🚦 CH 18 · CONCURRENCY CONTROL

여러 트랜잭션이 동시에 달려도
직렬화 가능하게 만드는 법

동시성 제어(concurrency control)는 여러 트랜잭션이 같은 데이터에 동시에 접근해도 결과가 직렬 가능(serializable)하도록 보장하는 메커니즘입니다. Lock 기반(2PL·교착상태·다중 단위), Timestamp·검증 기반(TSO·OCC), 그리고 다중버전(MVCC·Snapshot Isolation) 세 가지 접근을 시뮬레이터로 직접 돌려보며 익힙니다.

🔒 Lock 호환성 행렬 📈 2PL 단계 애니메이션 ⚔️ Wait-Die / Wound-Wait ⏱️ TSO 규칙 엔진 🌿 Write Skew

PART 1 Lock 기반 프로토콜

데이터 항목에 자물쇠(lock)를 걸어 동시 접근을 직접 제어한다. 가장 널리 쓰이는 방식.

🧭 이 파트에서 다루는 것

Lock 모드(S/X)와 호환성 → 2단계 잠금(2PL)으로 직렬가능성 보장 → 그 부작용인 교착상태(deadlock) 처리 → 동시성·오버헤드를 조절하는 다중 단위 잠금(multiple granularity) → 범위 질의의 함정 Phantom.

18.1 Lock 모드 & 호환성

Lock은 데이터 항목(data item)에 대한 동시 접근을 제어하는 메커니즘이다.

두 가지 Lock 모드

Shared (S) — 공유 lock
읽기(read)만 가능. lock-S(Q)로 요청. 여러 트랜잭션이 같은 항목에 동시에 S lock을 보유할 수 있다.
Exclusive (X) — 배타 lock
읽기(read)와 쓰기(write) 모두 가능. lock-X(Q)로 요청. 한 트랜잭션이 X lock을 보유하면 다른 어떤 lock도 불가.

동시성 제어 관리자(CC manager)가 lock 요청을 받아 허가(grant)하거나 대기시킨다.

왜 단순 locking으로는 부족한가

필요할 때 lock 걸고 끝나면 바로 unlock(unlock을 일찍)하는 단순 locking직렬가능성(serializability)을 보장하지 못한다. 한 트랜잭션이 unlock한 직후 다른 트랜잭션이 끼어들어 비직렬 스케줄이 만들어질 수 있기 때문. → 그래서 2PL이 필요하다(다음 절).

🔒 Lock 호환성 행렬 (Lock Compatibility) 인터랙티브
보유 중인 lock(행) vs 요청 lock(열)
셀을 클릭하면 이유를 설명합니다.
셀을 클릭하세요.
초록(✔)=호환(동시 보유 가능), 빨강(✘)=비호환(대기).
🎯 핵심 — Lock 호환성

S–S 만 호환(true)이다. S–X, X–S, X–X 는 모두 비호환(false). 즉 X lock이 걸려 있으면 다른 어떤 lock도 허가되지 않는다.

18.2 2단계 잠금 (Two-Phase Locking, 2PL)

모든 lock 획득이 모든 lock 해제보다 먼저 — 이 한 규칙이 conflict-serializability를 보장한다.

두 단계

Growing phase (성장)
lock을 획득(O)할 수 있고 해제(X)는 할 수 없다.
Shrinking phase (수축)
lock을 해제(O)할 수 있고 획득(X)은 할 수 없다.
Lock point
마지막 lock을 획득한 지점(= growing의 끝). lock point 순서로 트랜잭션이 직렬화된다.

강화된 변형

Strict 2PL
모든 X lock을 commit/abort 시점까지 유지. → cascadeless(연쇄 롤백 없음).
Rigorous 2PL
모든 lock(S, X)을 commit까지 유지. commit 순서로 직렬화 → recoverable + cascadeless. 대부분의 상용 DB가 채택.

2PL은 직렬가능성의 충분조건이지 필요조건은 아니다 — 2PL이 아니어도 conflict-serializable한 스케줄이 존재한다.

📈 2PL 단계 애니메이션 (보유 lock 수 vs 시간) 인터랙티브
▶ 재생을 눌러 Growing(상승) → lock point → Shrinking(하강) 사다리꼴을 확인하세요.

Lock conversion (변환)

Upgrade (S → X)
Growing phase에서만 가능. 읽다가 쓰기로 격상.
Downgrade (X → S)
Shrinking phase에서만 가능. 쓰기를 마치고 읽기로 강등.

2PL ≠ deadlock-free

2PL은 직렬가능성은 보장하지만 교착상태(deadlock)를 막지 못한다. 두 트랜잭션이 서로가 보유한 lock을 upgrade하려고 기다리면 순환 대기가 생긴다. → 다음 절에서 처리.

🎯 핵심 — 2PL

Growing(획득만)/Shrinking(해제만), lock point 순서로 직렬화. Strict=X lock을 commit까지, Rigorous=모든 lock을 commit까지(commit 순서 직렬화). upgrade=Growing, downgrade=Shrinking. 2PL은 deadlock-free가 아니다.

18.3 교착상태 (Deadlock)

집합 안의 모든 트랜잭션이 서로가 보유한 lock을 기다리며 영원히 멈춘 상태.

정의와 예

트랜잭션 집합 안의 모든 트랜잭션이 서로가 보유한 lock을 기다린다.

// 교착 예시
T3: lock-X(B)... write(B)  → 이제 lock-X(A) 대기
T4: lock-S(A)... read(A)   → 이제 lock-S(B) 대기
// T3는 A를(T4 보유), T4는 B를(T3 보유) 기다림 → 순환!

locking의 necessary evil(피할 수 없는 부작용). Starvation(기아)도 발생 가능.

Deadlock prevention (예방)

  • ① Pre-declaration — 실행 전에 필요한 모든 항목을 한꺼번에 lock.
  • ② Partial ordering — 데이터 항목에 순서를 정하고 그 순서대로만 lock(graph-based).
  • ③ Timestamp 기반 — Wait-Die / Wound-Wait (아래 시뮬레이터).
  • ④ Timeout — 일정 시간만 대기 후 rollback. 간단하나 불필요한 rollback·starvation 가능.
⚔️ Wait-Die / Wound-Wait 시뮬레이터 인터랙티브
판정 버튼을 누르세요. (TS가 작을수록 older=먼저 시작한 오래된 트랜잭션)
Wait-DieWound-Wait
선점(preemption)non-preemptivepreemptive
요청자가 older일 때기다림(wait)보유자(younger)를 wound=롤백
요청자가 younger일 때die=롤백기다림(wait)
롤백 발생량상대적으로 많음상대적으로 적음
restart 시원래 TS 유지 → starvation 방지
🔗 Wait-for Graph cycle 데모 (Deadlock detection) 인터랙티브
정점=트랜잭션, 간선 Ti→Tj = "Ti가 Tj가 보유한 lock을 기다림". 순환(cycle)이 생기면 deadlock.
ℹ️ Detection & Recovery

Detection: wait-for graph를 주기적으로 검사해 cycle ⟺ deadlock. Recovery: victim(희생자)을 최소 비용 기준으로 골라 total/partial rollback. 가장 오래된(oldest) 트랜잭션은 victim으로 선정하지 않아 starvation을 방지한다.

🎯 핵심 — 교착상태

Wait-Die: younger가 die(롤백), non-preemptive. Wound-Wait: older가 younger를 wound(롤백), preemptive. 둘 다 롤백 시 원래 TS로 restart(기아 방지). wait-for graph의 cycle = deadlock.

18.3+ 교착 탐지 알고리즘 심화 보충

예방(prevention) 대신 일단 두고, 주기적으로 wait-for graph의 cycle을 찾아 victim을 롤백한다.

Wait-for graph 구성 & cycle 탐지 (DFS)

정점=트랜잭션, 간선 Ti → Tj = "Ti가 Tj가 보유한 lock을 기다림". Ti의 lock 요청이 Tj가 보유한 lock과 충돌해 대기에 들어갈 때 간선을 추가하고, lock을 허가/해제하면 간선을 제거한다. cycle ⟺ deadlock.

// 주기적(periodic) cycle detection — DFS 3색
function hasCycle(G):
  color[v] = WHITE  // 모든 정점
  for v in G:
    if color[v] == WHITE:
      if dfs(v): return true
  return false

function dfs(u):
  color[u] = GRAY        // 재귀 스택 위
  for w in adj[u]:
    if color[w] == GRAY: return true  // back-edge=cycle
    if color[w] == WHITE and dfs(w): return true
  color[u] = BLACK       // 완료
  return false

호출 주기는 trade-off: 너무 자주=오버헤드↑, 너무 드물게=탐지 지연(불필요한 대기·잠재 starvation)↑. 보통 고정 주기 또는 대기 트랜잭션 수에 따라 호출.

Victim selection (희생자 선택) 비용함수

cycle이 발견되면 cycle을 깨기 위해 최소 비용(minimum cost) 트랜잭션을 골라 rollback한다. 비용 추정 요소:

  • ① 남은 작업 / 경과 시간 — 이미 한 일과 끝까지 남은 일이 적을수록 rollback 비용↓.
  • ② 사용·보유한 데이터 항목 수 (보유 lock 수).
  • ③ 완료에 더 필요한 항목 수.
  • ④ rollback에 연루되는 트랜잭션 수 (cascading).

Rollback 범위 & starvation 방지

  • Total rollback(abort 후 재시작) vs partial rollback(cycle을 깰 만큼만 되돌림).
  • Starvation 방지: 같은 트랜잭션이 반복해서 victim이 되면 영원히 진행 못 함 → 비용함수에 rollback 횟수(rollback count)를 포함시켜, 일정 횟수를 넘으면 victim 후보에서 제외(가장 오래된 트랜잭션 보호).
ℹ️ Detection vs Prevention

Prevention(wait-die/wound-wait, ordering)은 deadlock이 아예 생기지 않게 하지만 불필요한 rollback을 유발할 수 있다. Detection은 deadlock을 허용하되 사후에 깨므로 충돌이 드문 워크로드에서 유리하다. 위 Wait-for Graph 데모에서 직접 cycle을 만들어 보라.

🎯 핵심 — 탐지 & 복구

wait-for graph를 DFS(3색)로 주기 검사 → back-edge(GRAY) = cycle = deadlock. Victim은 rollback 비용 최소(보유 lock·남은 작업·연루 트랜잭션 수 기준)로 선택, rollback 횟수를 비용에 반영해 starvation을 막는다. total vs partial rollback.

18.4 다중 단위 잠금 (Multiple Granularity)

레코드 하나부터 DB 전체까지 — lock 단위를 고를 수 있게 해 동시성과 오버헤드를 절충한다.

계층(hierarchy)과 trade-off

DB > File > Table > Page > Record 의 트리. 어떤 노드를 lock하면 그 자손(descendant)이 같은 모드로 implicit하게 lock된다.

  • Fine granularity(작은 단위, 예: record): 동시성↑, lock 오버헤드↑.
  • Coarse granularity(큰 단위, 예: DB): 동시성↓, 오버헤드↓.

Intention locks (의도 lock)

IS
하위(descendant)에 S lock을 걸 의도가 있음.
IX
하위에 S 또는 X lock을 걸 의도가 있음.
SIX
이 subtree 전체를 S + 하위 일부에 X (= S + IX).
📊 5×5 Intention Lock 호환성 행렬 인터랙티브
보유(행) vs 요청(열)
셀을 클릭하면 호환 여부를 설명합니다.
셀을 클릭하세요.
🌲 Granularity Tree — record 하나를 X lock 하려면 인터랙티브
leaf record에 X를 걸려면 root→leaf 경로의 조상에 IX를 먼저 걸어야 한다(획득은 root→leaf, 해제는 leaf→root).

잠금 규칙(rules)

  • ① 호환성 행렬을 준수한다.
  • root를 먼저 lock하고, 어떤 모드로든 시작할 수 있다.
  • ③ Q를 S 또는 IS로 lock하려면 부모(parent)가 IX 또는 IS여야 한다.
  • ④ Q를 X · SIX · IX로 lock하려면 부모가 IX 또는 SIX여야 한다.
  • ⑤ two-phase: unlock 이전에만 lock할 수 있다.
  • ⑥ 자식이 lock되어 있지 않을 때만 노드를 unlock한다.

획득 순서 root→leaf, 해제 순서 leaf→root. fine lock이 너무 많아지면 coarse로 escalation한다.

🎯 핵심 — Intention 5×5

IS는 X만 비호환, IX는 IS·IX만 호환, S는 IS·S만 호환, SIX는 IS만 호환, X는 모두 비호환. record를 X 하려면 조상에 IX 경로를 깔아야 한다.

18.4+ 그래프 기반 / 트리 프로토콜 (Graph-Based / Tree Protocol) 보충

데이터 항목에 미리 정한 부분순서(partial order)를 부여하면, 2PL 없이도 직렬가능 + 교착 없는 프로토콜을 만들 수 있다.

Graph-based protocol

데이터 항목 집합에 부분순서(partial order)를 부여한다 — 방향 비순환 그래프(database graph, DAG). "di → dj 이면 di·dj 둘 다 접근하는 트랜잭션은 반드시 di를 먼저 접근한다"는 사전 지식(prior knowledge)을 활용한다.

Tree protocol은 이 그래프가 트리(rooted tree)인 가장 단순한 경우다.

Tree protocol 규칙

  • ① X-lock만 사용한다(이 단순형은 exclusive lock만).
  • ② 첫 lock은 임의의 노드에 걸 수 있다.
  • ③ 이후 노드 Q를 lock하려면 Q의 부모(parent)를 현재 Ti가 lock하고 있어야 한다.
  • ④ 언제든(at any time) unlock 할 수 있다(2PL의 shrinking 제약 없음).
  • ⑤ 한 번 unlock한 노드는 같은 트랜잭션이 다시 lock할 수 없다(re-lock 금지).
🌳 Tree Protocol 예시 — B,D를 거쳐 D·E를 lock 다이어그램

트랜잭션이 D와 E에 접근하려면, 트리 경로의 조상을 거치며(crab) lock해야 한다. 예: A→B를 lock한 뒤 B를 잡은 채 D를 lock, 그다음 B를 unlock해도 됨(④). 그러나 unlock한 B는 다시 lock 불가(⑤).

부모 lock을 보유한 동안에만 자식 lock이 허가되므로, lock 획득이 항상 위→아래 한 방향으로 흐른다 → 순환 대기(deadlock)가 원천적으로 불가능.

초록=현재 보유 lock, 회색 점선=이미 unlock(재lock 불가), 빨강 경로=lock 흐름.

장점(properties)

  • Conflict serializable 보장.
  • Deadlock-free — 대기가 한 방향이라 순환 불가 → rollback(wait-for) 불필요.
  • unlock을 일찍 할 수 있어 대기·lock 보유 시간↓ → 동시성↑.

단점(limitations)

  • Recoverability·cascade-freedom을 보장하지 않는다 → 별도 commit dependency(읽은 데이터를 쓴 트랜잭션이 commit할 때까지 대기) 필요.
  • 실제로 접근하지 않는 노드라도 경로상 조상이면 lock해야 할 수 있다 → 불필요한 lock 오버헤드.
  • 스케줄 중 일부 conflict-serializable한 것은 표현 불가(2PL과 서로 비교 불가능한 클래스).
🎯 핵심 — Tree Protocol

X-lock만, 첫 lock 임의, 이후 부모를 lock 중일 때만 자식 lock, 언제든 unlock, 재lock 금지. → conflict serializable + deadlock-free(rollback 불요). 단, recoverability/cascade는 보장 X(commit dependency 필요), 안 쓰는 조상도 lock해야 할 수 있음.

18.5 Phantom 현상 (Phantom Phenomenon)

"조건에 맞는 튜플 집합"을 읽었는데 누가 새 튜플을 끼워 넣으면 — 공통 튜플이 없어도 충돌이다.

문제

T1: SELECT count(*) FROM instructor
    WHERE dept = 'Physics';   -- predicate read
T2: INSERT instructor(..., dept='Physics');
    -- 같은 튜플을 건드린 적 없지만
    -- T1의 "조건 집합"을 바꿈 → conflict!

기존 튜플에 tuple lock만 걸어서는 막을 수 없다 → non-serializable.

해결: Index locking & Next-key

  • 모든 relation은 최소 하나의 index를 가진다고 가정.
  • Index locking: lookup은 해당 index leaf node를 S-lock, insert/delete는 X-lock (2PL 준수).
  • Next-key locking: lookup 조건을 만족하는 값 + 그 다음 키 값(next key)까지 함께 lock → 사이에 끼어드는 insert를 차단하면서 동시성↑.
📎 보충 — Phantom 보충

Phantom은 "이미 존재하는 데이터"가 아니라 "아직 존재하지 않는, 조건에 맞을 미래의 튜플"을 막아야 하는 문제다. 그래서 데이터가 아니라 접근 경로(index)에 lock을 거는 발상이 등장한다.

PART 2 Timestamp & Validation

lock 없이 — 트랜잭션에 시간표(timestamp)를 붙이거나, 끝나기 직전에 검증한다.

🧭 이 파트에서 다루는 것

각 트랜잭션에 timestamp를 부여하고 그 순서를 직렬화 순서로 강제하는 Timestamp Ordering(TSO), 그리고 충돌이 드물다고 낙관하고 commit 직전에만 검사하는 Optimistic Concurrency Control(OCC, 검증 기반).

18.6 타임스탬프 순서 (Timestamp Ordering, TSO)

트랜잭션 진입 순서(timestamp)가 곧 직렬화 순서다. 어기는 연산은 거부·롤백.

정의

TS(Ti)
트랜잭션 Ti가 시스템에 진입할 때 부여되는 고유 timestamp. 나중 트랜잭션이 strictly larger(logical counter도 가능).
W-timestamp(Q)
Q에 성공적으로 write한 트랜잭션들의 TS 중 최댓값.
R-timestamp(Q)
Q를 성공적으로 read한 트랜잭션들의 TS 중 최댓값.

목표: timestamp 순서 = 직렬화 순서.

규칙(rules)

READ(Ti): if TS(Ti) < W-TS(Q) → reject, rollback Ti else 실행, R-TS(Q) = max(R-TS, TS(Ti))
WRITE(Ti): if TS(Ti) < R-TS(Q) → reject, rollback elif TS(Ti) < W-TS(Q) → obsolete → reject, rollback (Thomas: ignore) else 실행, W-TS(Q) = TS(Ti)
⏱️ TSO 규칙 시뮬레이터 (규칙 엔진) 인터랙티브
항목별 timestamp
실행 로그
아직 연산 없음.
연산을 만들어 ▶ 적용을 누르거나, 예제 버튼으로 미리 짜인 스케줄을 실행하세요.
ℹ️ TSO의 성질

serializable(모든 충돌 간선이 작은 TS→큰 TS, precedence graph가 acyclic) · deadlock-free(대기가 없음, 즉시 거부) · 하지만 recoverable·cascade-free는 보장하지 않는다(추가 장치 필요).

🎯 핵심 — TSO & Thomas's Write Rule

READ는 TS(Ti)<W-TS(Q)면 거부. WRITE는 TS(Ti)<R-TS(Q) 또는 TS(Ti)<W-TS(Q)면 거부. Thomas's Write Rule: obsolete write(TS(Ti)<W-TS(Q))를 롤백하지 않고 무시(ignore) → 더 높은 동시성, view-serializable 허용.

18.7 검증 기반 프로토콜 (OCC, Optimistic Concurrency Control)

"충돌은 드물다"고 낙관 — 일단 실행하고 commit 직전에만 검증한다.

3 단계 (3 phases)

① Read & execution
DB에서 read하고 연산 수행. write는 DB가 아닌 local 변수에만 한다.
② Validation
다른 트랜잭션과 충돌하는지 검증 테스트(validation test)를 수행.
③ Write
통과하면 local 변수를 DB에 반영, 실패하면 rollback.

3 timestamp

StartTS(Ti)
read 단계 시작 시각.
ValidationTS(Ti)
validation 단계 진입 시각. 이 값이 직렬화 순서 TS(Ti).
FinishTS(Ti)
write 단계 완료 시각.

commit time을 직렬화 순서로 삼는다. 충돌이 낮으면 동시성↑.

Validation test (Tj 검증) 보충

TS(Ti) < TS(Tj) 인 모든 Ti에 대해 다음 중 하나가 성립해야 통과:

(a) finishTS(Ti) < startTS(Tj) → Ti가 Tj 시작 전에 완전히 끝남, OR (b) startTS(Tj) < finishTS(Ti) < validationTS(Tj) 그리고 Tj가 Ti의 write set을 read하지 않음
🎯 핵심 — OCC

3 phase = read(local write) → validation(test) → write(DB 반영/rollback). 3 timestamp = Start/Validation/Finish, TS=ValidationTS. 충돌이 적은 워크로드에서 lock보다 유리.

PART 3 다중버전 (Multiversion)

덮어쓰지 말고 — 버전을 남겨 읽기와 쓰기가 서로를 막지 않게 한다.

🧭 이 파트에서 다루는 것

한 논리적 객체에 여러 물리적 버전을 두는 MVCC, 그리고 트랜잭션 시작 시점의 일관된 스냅샷을 읽는 Snapshot Isolation(SI) — 그 강력함과 대표적 약점 Write Skew.

18.8 다중버전 동시성 제어 (MVCC)

write는 새 버전을 만들고, read는 자기 시점의 옛 버전을 읽는다 — 서로 비차단.

버전과 가시성

하나의 논리적 객체가 여러 물리적 버전을 가진다. 각 버전은 begin-TS, end-TS를 갖는다.

가시성 규칙: Ti는 begin-TS ≤ TS(Ti) < end-TS 인 버전을 읽는다. (미커밋 버전은 비가시)
  • Writers don't block readers / readers don't block writers.
  • write-write 충돌 시 상대가 commit할 때까지 wait.

MV2PL 보충

update 트랜잭션은 rigorous 2PL로 동작(read=최신 committed). 새 버전 TS=∞로 만들고 commit 시 ts-counter+1로 확정하며 카운터 증가. read-only 트랜잭션은 버전을 통해 lock 없이 읽는다.

🪢 MVCC 버전 체인 — read-only T1 vs writer T2 인터랙티브
Database 버전 테이블
트랜잭션 상태(TRX)
▶ 다음 단계로 시나리오 A를 진행하세요.
T1(begin-TS=1, read-only)은 줄곧 A₀=111을 읽고, T2(begin-TS=2)가 A₁=222를 만들어도 T1의 read는 변하지 않는다.
🎯 핵심 — MVCC

가시성 규칙 begin-TS ≤ TS(Ti) < end-TS. writer가 새 버전을 만들어도 read-only는 옛 버전(A₀=111)을 계속 본다 → reader/writer 비차단.

18.8+ 다중버전 타임스탬프 순서 (Multiversion Timestamp Ordering, MVTO) 보충

TSO를 다중버전 위에서 돌린다 — 읽기는 적절한 옛 버전을 골라 읽으므로 절대 거부되지 않는다.

버전과 timestamp

각 데이터 항목 Q는 버전열 Q1, Q2, …, Qk를 가진다. 각 버전 Qi는:

content
Qi가 담은 값.
W-timestamp(Qi)
이 버전을 만든 트랜잭션의 TS.
R-timestamp(Qi)
이 버전을 읽은 트랜잭션들의 TS 중 최댓값.

규칙(rules) — Ti, TS=TS(Ti)

read(Q): W-TS ≤ TS(Ti) 인 버전 중 W-TS가 가장 큰 버전 Qk를 읽는다. → read는 절대 거부되지 않음(never rejected) R-TS(Qk) = max(R-TS(Qk), TS(Ti))
write(Q): (Ti가 읽을 버전 = Qk) if R-TS(Qk) > TS(Ti) → rollback Ti elif W-TS(Qk) = TS(Ti) → Qk 덮어쓰기 else → 새 버전 생성(W-TS=R-TS=TS(Ti))
🪜 MVTO read/write 단계 데모 인터랙티브
Q 버전열 (versions of Q)
실행 로그
아직 연산 없음.
read는 항상 적절한 옛 버전을 읽어 거부되지 않는다. write만 R-TS(Q_k) > TS(Ti)일 때 rollback.
ℹ️ 기본 TSO와의 차이 & 한계

기본 TSO는 read가 TS(Ti)<W-TS(Q)면 거부되지만, MVTO는 옛 버전을 읽어 read를 절대 거부하지 않는다(읽기 비차단). 단 MVTO 자체는 recoverable이 아니다 → 읽은 버전을 만든 트랜잭션이 commit할 때까지 read commit을 미루는 등 MV2PL / strict 변형으로 보완한다.

🎯 핵심 — MVTO

read는 never rejected(W-TS ≤ TS 중 최신 버전을 읽고 R-TS 갱신). write는 읽을 버전 Qk에 대해 R-TS(Qk) > TS(Ti)면 rollback, W-TS(Qk)=TS(Ti)면 덮어쓰기, 아니면 새 버전. recoverable은 보장 X.

18.9 Snapshot Isolation (SI)

읽기는 절대 막히지 않는다 — 하지만 직렬가능성이 깨지는 함정 Write Skew가 있다.

동작과 장점

  • 트랜잭션은 시작 시점의 일관된 스냅샷(consistent snapshot)을 읽는다(no torn writes).
  • First-committer-wins: 같은 객체를 두 트랜잭션이 update하면 늦게 commit하는 쪽이 abort.
  • 장점: read가 절대 차단되지 않음, read committed급 성능, dirty read·non-repeatable read 없음.

단점: serializability 깨짐

SI는 강력하지만 완전한 직렬가능성은 보장하지 않는다. first-committer-wins는 같은 객체에 대한 충돌만 잡는다. 서로 다른 객체를 쓰면 못 잡는다 → Write Skew.

🥇 First-Committer-Wins 데모 (같은 객체 X) 인터랙티브
T1, T2가 모두 X=100을 읽고 각자 X에 쓴다. 먼저 commit한 쪽이 이긴다.
🌿 ★Write Skew★ 데모 (서로 다른 객체) 인터랙티브
T1 local workspace
(A := B)
Database (committed)
T2 local workspace
(B := A)
초기값 A=1, B=10. T1은 A:=B, T2는 B:=A를 SI 하에서 동시에 수행한다.
두 트랜잭션이 서로 다른 항목(T1→A, T2→B)을 쓰므로 first-committer-wins가 충돌을 감지하지 못한다.
🎯 핵심 — ★Write Skew★

초기 A=1, B=10. SI 하에서 T1: A:=B, T2: B:=A를 동시에 실행하면 각자 스냅샷으로 계산해 최종 A=10, B=1. 어떤 직렬 순서에서도 나올 수 없는 결과(직렬이면 A=B여야 함). 서로 다른 항목을 써서 first-committer-wins가 못 잡는, SI의 대표적 약점.

18.9+ 직렬가능 스냅샷 격리 (Serializable Snapshot Isolation, SSI) 보충·심화

SI에 읽기-쓰기 반의존(rw-antidependency) 추적을 더해, 위험한 구조가 보이면 한 트랜잭션을 abort — 읽기 비차단은 유지하면서 직렬성을 회복한다.

문제 → 발상

SI는 Write Skew로 직렬성이 깨진다. 직렬성 위반은 항상 트랜잭션 충돌 그래프(serialization graph)의 cycle인데, SI 하에서 발생하는 cycle에는 반드시 두 개의 연속된 rw-antidependency 간선이 들어 있음이 알려져 있다(Adya, Fekete et al.).

rw-antidependency
(읽기-쓰기 반의존)
Ti가 어떤 데이터를 읽은 뒤, Tj가 같은 데이터의 새 버전을 쓰는데, Tj의 write가 Ti의 read 이후에 와서 Ti가 그 새 값을 못 본 관계. 표기 Ti --rw--> Tj.

Dangerous structure & pivot

SSI는 실행 중 in/out rw-간선 플래그를 추적한다. 한 트랜잭션 Tpivot이 들어오는 rw-간선과 나가는 rw-간선을 동시에 가지면 — 즉

Tin --rw--> Tpivot --rw--> Tout

dangerous structure가 탐지되면 cycle 가능성이 있으므로, 세 트랜잭션 중 하나를 abort(보통 pivot 또는 더 나중 것)한다. 실제 cycle이 아닐 수도 있어 → false-positive abort가 발생할 수 있는 보수적(conservative) 방식.

🔺 SSI rw-antidependency 다이어그램 (Write Skew를 막는 법) 다이어그램

Write Skew(A=1,B=10; T1:A:=B, T2:B:=A)에서 — T1은 B를 읽었는데 T2가 B에 쓴다(T1--rw-->T2), T2는 A를 읽었는데 T1이 A에 쓴다(T2--rw-->T1).

두 rw-간선이 양방향으로 맞물려 dangerous structure를 이룬다 → SSI가 탐지해 한 트랜잭션을 abort → 직렬성 회복. 읽기는 여전히 스냅샷에서 비차단으로 수행된다.

붉은 점선 = rw-antidependency 간선. 두 간선이 한 사이클을 이루면 SSI가 개입한다.
ℹ️ 실무 — PostgreSQL SERIALIZABLE

PostgreSQL의 SERIALIZABLE 격리수준은 SSI 구현(Cahill, Röhm, Fekete 2008; "Serializable Isolation for Snapshot Databases"). 장점: 읽기 잠금 없이 SI의 성능을 유지하면서 완전한 직렬성을 제공. 비용: 추적 메타데이터 + false-positive로 인한 직렬화 실패(serialization_failure) 재시도가 늘 수 있다(앱에서 retry 필요).

🎯 핵심 — SSI

SSI = SI + rw-antidependency 추적. 한 트랜잭션이 두 rw-간선의 pivot(T_in--rw-->T_pivot--rw-->T_out, dangerous structure)이 되면 abort → Write Skew 같은 비직렬 스케줄을 차단. 읽기 비차단 유지 ↔ false-positive abort. PostgreSQL SERIALIZABLE이 대표 구현.

18.9++ 약한 일관성 & 실무 (Weak Consistency & Practice) 보충

현실의 DBMS는 직렬성과 동시성 사이에서 격리수준을 골라 쓴다 — 무엇을 포기하는지가 핵심.

OLTP vs OLAP 일관성 요구

  • OLTP(짧은 갱신 트랜잭션 多): 정확성이 중요 → 강한 격리(보통 RC/RR, 자금 이체 등은 직렬성). 동시성 throughput이 핵심.
  • OLAP / 리포팅(대규모 read-only 분석): 약간의 비일관성을 감수하고 read-only · 낮은 격리(또는 스냅샷)로 lock 경합을 피한다.

격리수준 ↔ 동시성 trade-off

격리수준을 낮추면 허용 이상현상(dirty/non-repeatable read, phantom)↑·동시성↑·오버헤드↓. 높이면 정확성↑·동시성↓.

2PL이 보장하는 것 / 못 하는 것

  • 보장: conflict serializability(기본 2PL). Strict/Rigorous는 추가로 recoverable·cascadeless.
  • 못 함: deadlock-free가 아니다(별도 prevention/detection 필요). 또한 phantom은 tuple lock만으로는 못 막아 index/next-key locking이 필요.

즉 "2PL을 쓰면 끝"이 아니라, deadlock 처리·phantom 방지를 함께 갖춰야 실용적 격리가 완성된다.

DBMS기본 격리수준(default)최고 격리 구현 방식비고
PostgreSQLRead CommittedSERIALIZABLE = SSI (MVCC 기반)읽기 비차단
MySQL (InnoDB)Repeatable ReadSERIALIZABLE = 2PL(공유락) + next-key lockRR에서도 next-key로 phantom 상당 방지
OracleRead CommittedSERIALIZABLE = Snapshot Isolation엄밀히는 SI(완전 직렬성 아님)
SQL ServerRead CommittedSERIALIZABLE = 2PL(range lock) / SNAPSHOT 옵션 = SI옵션으로 MVCC
⚠️ 표준 vs 구현 주의

ANSI SQL 격리수준(RU/RC/RR/Serializable)은 이상현상으로 정의되며, 같은 이름이라도 DBMS마다 구현(2PL·MVCC·SI·SSI)이 달라 동작이 다르다. 특히 Oracle의 SERIALIZABLE은 실제로는 SI여서 write skew가 가능하다.

🎯 핵심 — 약한 일관성 & 실무

격리수준을 낮출수록 동시성↑·정확성↓. 기본값: PostgreSQL/Oracle/SQL Server = Read Committed, MySQL InnoDB = Repeatable Read. SERIALIZABLE 구현은 PostgreSQL=SSI, Oracle=SI(주의), 그 외 2PL+range/next-key. 2PL은 conflict serializable만 보장, deadlock은 못 막는다.

18.10 핵심 정리

Lock 기반 (Part 1)

  • 호환성: S–S만 true, X는 모두 false.
  • 2PL = Growing/Shrinking, lock point 순서 직렬화. Strict(X→commit), Rigorous(all→commit).
  • upgrade=Growing, downgrade=Shrinking. 2PL은 deadlock-free 아님.
  • Wait-Die(younger die, non-preemptive) / Wound-Wait(older wound, preemptive). wait-for graph cycle=deadlock.
  • Intention 5×5, record X엔 IX 경로. Phantom→index/next-key locking.

Timestamp·검증·다중버전 (Part 2·3)

  • TSO: READ TS<W-TS 거부 / WRITE TS<R-TS·TS<W-TS 거부. Thomas: obsolete write 무시.
  • TSO는 serializable·deadlock-free, recoverable은 보장X.
  • OCC: read→validation→write, TS=ValidationTS.
  • MVCC 가시성: begin-TS ≤ TS(Ti) < end-TS, reader/writer 비차단.
  • SI: first-committer-wins(같은 객체). 약점 ★Write Skew → A=10, B=1★.
🎯 시험 최빈출 체크리스트

① Lock 호환성(S-S만) · ② 2PL 단계/lock point/Strict·Rigorous/upgrade=Growing·downgrade=Shrinking · ③ Wait-Die vs Wound-Wait · wait-for cycle · ④ Intention 5×5 · ⑤ TSO read/write 부등식 + Thomas's rule · ⑥ OCC validation test · ⑦ MVCC 가시성 규칙 · ⑧ ★Write Skew(A=10,B=1)★.