--- id: cs-mvcc-concurrency title: MVCC — Multi-Version Concurrency Control category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [cs, database, mvcc, vibe-coding] tech_stack: { language: "Concept", applicable_to: ["Database"] } applied_in: [] aliases: [MVCC, snapshot isolation, serializable, transaction visibility, xmin xmax] --- # MVCC > Postgres / MySQL InnoDB / SQL Server 의 동시성. **각 transaction 가 자체 snapshot 봄**. Read 가 write 안 차단. 단 vacuum / cleanup 필요. ## 📖 핵심 개념 - 매 row = 여러 version (xmin, xmax). - Transaction 가 시작 시점 snapshot. - Read = 자기 snapshot 만 봄. - Write = 새 version 만들고 옛 version 은 dead tuple. ## 💻 코드 패턴 ### 기본 동작 (Postgres) ```sql -- T1 시작 BEGIN; SELECT balance FROM accounts WHERE id = 1; -- 100 -- T2 (다른 connection) BEGIN; UPDATE accounts SET balance = 200 WHERE id = 1; COMMIT; -- T1 다시 read SELECT balance FROM accounts WHERE id = 1; -- 100 (자기 snapshot) COMMIT; -- 새 connection SELECT balance FROM accounts WHERE id = 1; -- 200 ``` → Read 가 write 안 차단. 일관 view. ### Isolation level ```sql -- Read committed (default Postgres) BEGIN ISOLATION LEVEL READ COMMITTED; -- 매 statement 가 fresh snapshot -- Repeatable read BEGIN ISOLATION LEVEL REPEATABLE READ; -- Transaction 시작 시 snapshot — 같은 query = 같은 결과 -- Serializable BEGIN ISOLATION LEVEL SERIALIZABLE; -- 가장 강. Conflict 시 abort ``` ### xmin / xmax ```sql SELECT xmin, xmax, * FROM accounts WHERE id = 1; -- xmin = 만든 transaction id -- xmax = 삭제 / 변경한 transaction id (0 = 활성) ``` ```sql -- Tuple visible 조건: -- 1. xmin 가 commit 됨 + 자기 snapshot 보다 작거나 같음 -- 2. xmax 가 0 또는 (commit 안 됨 OR 자기 snapshot 보다 큼) ``` ### Dead tuple ```sql -- UPDATE -- 옛 row: xmax = 1234 (삭제됨) -- 새 row: xmin = 1234 -- DELETE -- xmax = 1234 (삭제됨) -- Vacuum 가 dead tuple 정리. ``` → [[DB_Vacuum_Autovacuum]]. ### Read consistency 예 ```sql -- T1: BEGIN; SELECT 1000개 row; -- T2 (중간): UPDATE / DELETE 일부 -- T1: 같은 1000개 row 봄 (snapshot) ``` → Backup / report 시 일관 view. ### Write conflict (concurrent update) ```sql -- T1: BEGIN; UPDATE x SET v = v + 1 WHERE id = 1; -- T2 (동시): UPDATE x SET v = v + 1 WHERE id = 1; -- T2 가 wait (lock) -- T1 commit → T2 진행 -- 결과: v = 2 (둘 다 적용) -- Serializable 시 T2 abort 가능 (write skew): -- T1 read x = 5; UPDATE based on 5 -- T2 read x = 5; UPDATE based on 5 (다른 row) -- → Inconsistent — Serializable 가 detect + abort ``` ### SELECT FOR UPDATE ```sql BEGIN; SELECT * FROM accounts WHERE id = 1 FOR UPDATE; -- row lock -- 다른 transaction 의 같은 row write 차단 UPDATE accounts SET balance = balance - 100 WHERE id = 1; COMMIT; ``` ### Optimistic concurrency (version) ```sql -- 옛 schema ALTER TABLE accounts ADD COLUMN version INT NOT NULL DEFAULT 0; -- App async function transfer(id: string, amount: number, expectedVersion: number) { const r = await db.execute(` UPDATE accounts SET balance = balance + $1, version = version + 1 WHERE id = $2 AND version = $3 `, [amount, id, expectedVersion]); if (r.rowCount === 0) throw new ConcurrencyError(); } ``` → Lock 없이 conflict 검출. ### MVCC 의 장점 ``` + Read 가 write 차단 X + 일관 view (snapshot) + Reader / writer 동시 + Backup / report 가 production 안 멈춤 ``` ### 단점 ``` - Dead tuple 누적 (vacuum 필요) - Write 가 새 row 만듦 (in-place X) - Visibility 검사 overhead (작음) - Long transaction 가 vacuum 차단 ``` ### Bloat (MVCC 의 비용) ```sql -- 자주 UPDATE = bloat -- HOT update (index 컬럼 안 변경) = 같은 page 안 — bloat 적음 -- 측정 SELECT relname, n_dead_tup, n_live_tup, round(100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0), 2) AS dead_pct FROM pg_stat_user_tables ORDER BY n_dead_tup DESC; ``` → [[DB_Vacuum_Autovacuum]]. ### Long transaction = autovacuum 차단 ``` T1: 30분 transaction (read-only OK). T1 의 snapshot 가 옛 — 그 시점부터 dead tuple 정리 못 함. → 다른 table 도 bloat 자라남. ``` ```sql -- 검출 SELECT pid, age(NOW(), xact_start) FROM pg_stat_activity WHERE state != 'idle' AND xact_start < NOW() - INTERVAL '10 minutes'; ``` ### Snapshot isolation vs Serializable ``` Snapshot: Read consistent — but write skew 가능. Serializable: Write skew 도 차단 — abort + retry. Postgres SSI (Serializable Snapshot Isolation): - Snapshot + serialize order detection - Conflict 시 abort ``` ### Write skew 예 ```sql -- 두 의사 가 둘 다 on-call 가능. -- 1명 이상 항상 on-call 보장. -- T1: SELECT count(*) FROM doctors WHERE on_call = true; -- 2 -- T2: SELECT count(*) FROM doctors WHERE on_call = true; -- 2 -- T1: UPDATE doctors SET on_call = false WHERE id = 'A'; -- T2: UPDATE doctors SET on_call = false WHERE id = 'B'; -- 둘 다 commit -- 결과: on_call 0명 — invariant 깨짐 ``` → Snapshot isolation 가 detect 못 함. Serializable 가 한 쪽 abort. ```sql BEGIN ISOLATION LEVEL SERIALIZABLE; -- T1, T2 둘 다 위 시도 → 한 명 abort + retry COMMIT; ``` ### MySQL InnoDB ``` Repeatable read (default). Phantom read 검출 X — 단 InnoDB 의 next-key lock 가 부분 방어. ``` ### CockroachDB ``` Serializable default. Strong consistency — 분산. ``` ### MVCC 가 없는 DB ``` SQLite: rollback journal — write 가 read 차단. Old MySQL MyISAM: table lock. ``` ### App 측 패턴 ```ts async function transfer(from: string, to: string, amount: number) { return db.transaction(async (tx) => { const sender = await tx.execute('SELECT balance FROM accounts WHERE id = $1 FOR UPDATE', [from]); if (sender.balance < amount) throw new Error('insufficient'); await tx.execute('UPDATE accounts SET balance = balance - $1 WHERE id = $2', [amount, from]); await tx.execute('UPDATE accounts SET balance = balance + $1 WHERE id = $2', [amount, to]); }); } ``` → FOR UPDATE = pessimistic. 또는 optimistic version. ### Retry on serializable ```ts async function withRetry(fn: () => Promise, max = 3): Promise { for (let i = 0; i < max; i++) { try { return await fn(); } catch (e) { if (e.code === '40001' /* serialization_failure */) continue; throw e; } } throw new Error('max retries'); } ``` ## 🤔 의사결정 기준 | 상황 | Isolation | |---|---| | 일반 OLTP | Read Committed (default) | | Long report | Repeatable Read | | Strong invariant | Serializable + retry | | Counter (concurrent) | UPDATE + atomic | | Transfer (atomic balance) | FOR UPDATE 또는 Serializable | ## ❌ 안티패턴 - **Long transaction (10분+)**: vacuum 차단 + bloat. - **Read 결과 그대로 저장 + 변경 X**: idempotent 깨짐. - **Concurrent UPDATE 무 lock**: lost update. - **Serializable 가정 + retry 없음**: random failure. - **Optimistic 가정 + version 없음**: 검출 X. - **Vacuum off**: dead tuple 폭발. - **Dead tuple 무관심**: query 점차 느려짐. ## 🤖 LLM 활용 힌트 - Postgres / MySQL = MVCC (snapshot). - Serializable + retry = 강한 안전. - FOR UPDATE = pessimistic. - Long transaction 피하기. ## 🔗 관련 문서 - [[DB_Transaction_Isolation]] - [[DB_Lock_Analysis]] - [[DB_Vacuum_Autovacuum]] - [[Optimistic_Concurrency_Control]]