[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,292 @@
|
||||
---
|
||||
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<T>(fn: () => Promise<T>, max = 3): Promise<T> {
|
||||
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]]
|
||||
Reference in New Issue
Block a user