--- id: db-transaction-patterns-deep title: DB Transaction Deep β€” isolation / SAVEPOINT / advisory lock category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [database, transaction, vibe-coding] tech_stack: { language: "SQL", applicable_to: ["Database"] } applied_in: [] aliases: [transaction, isolation, savepoint, advisory lock, nested transaction, explicit lock] --- # DB Transaction Deep > ACID 의 μ •λ°€ control. **Isolation, SAVEPOINT, advisory lock, FOR UPDATE**. ## πŸ“– 핡심 κ°œλ… - ACID: Atomic, Consistent, Isolated, Durable. - Isolation level (4 κ°€μ§€). - SAVEPOINT = nested transaction. - Advisory lock = app-level. ## πŸ’» μ½”λ“œ νŒ¨ν„΄ ### Basic transaction ```sql BEGIN; UPDATE accounts SET balance = balance - 100 WHERE id = 1; UPDATE accounts SET balance = balance + 100 WHERE id = 2; COMMIT; -- λ˜λŠ” ROLLBACK; ``` ### Isolation levels ``` READ UNCOMMITTED: dirty read κ°€λŠ₯ (Postgres κ°€ μ•ˆ implement). READ COMMITTED: dirty read μ•ˆ (Postgres default). REPEATABLE READ: phantom μ•ˆ (range query OK). SERIALIZABLE: κ°€μž₯ strict (slow). ``` ```sql SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; BEGIN; -- ... COMMIT; ``` ### SAVEPOINT (nested) ```sql BEGIN; INSERT INTO orders (...) VALUES (...); SAVEPOINT after_order; INSERT INTO items (...) VALUES (...); -- Error ROLLBACK TO after_order; -- λ§€ order 보쑴, items 만 rollback. COMMIT; ``` β†’ Partial rollback. ### Explicit lock (FOR UPDATE) ```sql BEGIN; SELECT * FROM accounts WHERE id = 1 FOR UPDATE; -- Other transaction κ°€ wait. UPDATE accounts SET balance = balance - 100 WHERE id = 1; COMMIT; ``` β†’ Exclusive row lock. ### FOR UPDATE SKIP LOCKED (queue) ```sql SELECT * FROM jobs WHERE status = 'pending' ORDER BY created_at LIMIT 1 FOR UPDATE SKIP LOCKED; -- β†’ Lock 된 row κ°€ skip. Multi-worker queue. ``` ### FOR SHARE ```sql SELECT * FROM users WHERE id = 1 FOR SHARE; -- β†’ Shared lock. Read κ°€ OK, write κ°€ wait. ``` ### Advisory lock (app-level) ```sql -- Acquire SELECT pg_advisory_lock(12345); -- Critical section -- ... -- Release SELECT pg_advisory_unlock(12345); ``` β†’ Cross-transaction. Cron / migration μΉœν™”. ### Transaction-scoped advisory ```sql SELECT pg_advisory_xact_lock(12345); -- β†’ Transaction 끝 μ‹œ μžλ™ release. ``` ### Distributed lock (Postgres) ```ts async function withDistributedLock(key: number, fn: () => Promise): Promise { const client = await pool.connect(); try { await client.query('SELECT pg_advisory_lock($1)', [key]); return await fn(); } finally { await client.query('SELECT pg_advisory_unlock($1)', [key]); client.release(); } } ``` β†’ Single Postgres = μž‘μ€ system 의 distributed lock. ### Optimistic locking (version) ```sql -- Version column UPDATE users SET name = 'Bob', version = version + 1 WHERE id = 1 AND version = 5; -- Affected rows = 0 β†’ conflict. ``` β†’ Lock μ—†μŒ. Retry on conflict. ### Read-write split (transaction) ```ts // Read replica const reader = pool.replica; const writer = pool.primary; // Write transaction await writer.transaction(async (tx) => { await tx.query('UPDATE ...'); }); // Read after write 의 함정: // Write tx commit β†’ replica 에 lag. // β†’ Read κ°€ stale. ``` β†’ Sticky reads. ### Long transaction 의 함정 ```sql BEGIN; SELECT * FROM users LIMIT 1; -- 1 hour ν›„ COMMIT; ``` β†’ Vacuum κ°€ freeze X. Bloat λˆ„μ . ```sql -- Auto kill SET statement_timeout = '30s'; SET idle_in_transaction_session_timeout = '60s'; ``` ### Deadlock ``` Tx A: lock row 1, wait row 2. Tx B: lock row 2, wait row 1. β†’ Postgres detect + abort 1. ``` ```ts try { await tx.query(...); } catch (e) { if (e.code === '40P01') { // Deadlock β€” retry. } } ``` ### Deadlock λ°©μ§€ ``` - λͺ¨λ“  transaction κ°€ 같은 order 둜 lock. - Short transaction. - μ΅œμ†Œ lock. ``` ### Two-phase commit (XA, 2PC) ```sql PREPARE TRANSACTION 'tx-1'; -- λ‹€λ₯Έ system 도 PREPARE. -- Coordinator κ°€ λͺ¨λ‘ OK. COMMIT PREPARED 'tx-1'; -- λ˜λŠ” ROLLBACK PREPARED 'tx-1'; ``` β†’ Cross-DB. 맀우 느린. 거의 μ•ˆ μ‚¬μš©. β†’ [[Backend_Saga_Choreography_vs_Orchestration]] κ°€ modern. ### Idempotent transaction ```sql INSERT INTO transactions (id, ...) VALUES ($idempKey, ...) ON CONFLICT (id) DO NOTHING; -- Retry-safe. ``` ### Outbox pattern ```sql BEGIN; INSERT INTO orders (...); INSERT INTO outbox (event_type, payload) VALUES ('OrderCreated', ...); COMMIT; -- Background process: outbox β†’ publish. ``` β†’ DB write + event κ°€ atomic. β†’ [[Backend_Outbox_Pattern]]. ### MVCC (Multi-Version Concurrency Control) ``` Postgres / MySQL InnoDB: - λ§€ row κ°€ version κ°€ 가짐. - Read κ°€ lock X (snapshot). - Write κ°€ μƒˆ version. β†’ "Reader κ°€ writer κ°€ 막지 μ•ŠμŒ". ``` β†’ [[CS_MVCC_Concurrency]]. ### Connection pool + transaction ```ts // λ§€ transaction κ°€ dedicated connection. const client = await pool.connect(); try { await client.query('BEGIN'); await client.query('UPDATE ...'); await client.query('COMMIT'); } finally { client.release(); } // λ˜λŠ” helper await pool.transaction(async (tx) => { await tx.query('UPDATE ...'); }); ``` ### PgBouncer 함정 (transaction mode) ``` PgBouncer transaction mode: - λ§€ transaction = λ‹€λ₯Έ connection. - Session-bound feature μ•ˆ 됨 (LISTEN, prepared, advisory lock). β†’ Session mode λ˜λŠ” raw query. ``` β†’ [[DB_Connection_Pooling_Patterns]]. ### Phantom read (REPEATABLE READ κ°€ λ§‰μŒ) ``` Tx A: SELECT count(*) FROM users WHERE age > 18 β†’ 100. Tx B: INSERT new user (age 20). Tx A: SELECT count(*) β†’ 101 (REPEATABLE READ X) / 100 (REPEATABLE READ). ``` ### Serializable conflict ```sql -- Postgres detect + abort. ERROR: could not serialize access due to concurrent update SQLSTATE: 40001 ``` β†’ Retry on 40001. ### Performance ``` SERIALIZABLE: κ°€μž₯ slow. REPEATABLE READ: μ•½κ°„ slow. READ COMMITTED: default + 빠름. β†’ Use case 의 적절. ``` ### Real-world tip ``` 1. Default = READ COMMITTED. 2. Transfer / financial = SERIALIZABLE λ˜λŠ” explicit lock. 3. Long read = SET TRANSACTION READ ONLY. 4. Idempotency key λ§€ critical write. 5. Optimistic lock + version (web app). 6. Advisory lock (distributed lock). ``` ### Distributed transaction (saga) ``` Cross-DB / cross-service: - 2PC μ•ˆ μ‚¬μš© (slow, brittle). - Saga + compensation. - Idempotent step. ``` β†’ [[Backend_Saga_Choreography_vs_Orchestration]]. ### 함정 ``` - Long transaction: bloat / lock contention. - Implicit transaction: ORM 의 auto-commit λ¬΄μ‹œ. - Lock order 닀름: deadlock. - Retry λ¬΄ν•œ: λ‚΄λ €. - 2PC: 느린, 거의 μ•ˆ μ‚¬μš©. ``` ## πŸ€” μ˜μ‚¬κ²°μ • κΈ°μ€€ | μž‘μ—… | μΆ”μ²œ | |---|---| | Read | Default isolation | | Critical write | SERIALIZABLE / FOR UPDATE | | Queue | FOR UPDATE SKIP LOCKED | | Web app | Optimistic + version | | Distributed lock | pg_advisory_lock | | Cross-service | Saga (NOT 2PC) | | Idempotent | Idempotency key | ## ❌ μ•ˆν‹°νŒ¨ν„΄ - **Long transaction**: bloat. - **2PC**: brittle. - **No retry on serialize fail**: lost. - **λͺ¨λ“  κ±° SERIALIZABLE**: slow. - **Lock order inconsistent**: deadlock. - **No timeout**: hang. ## πŸ€– LLM ν™œμš© 힌트 - READ COMMITTED κ°€ default. - FOR UPDATE SKIP LOCKED κ°€ queue 의 λ‹΅. - Advisory lock κ°€ app-level. - Saga > 2PC. ## πŸ”— κ΄€λ ¨ λ¬Έμ„œ - [[DB_Transaction_Isolation]] - [[DB_Lock_Analysis]] - [[CS_MVCC_Concurrency]]