LabHub
배우기 러닝패스 코스

並行性 — 二人が同じ行を触るとき

二つのセッションでぶつけてみる

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

혼자 돌릴 때는 맞는데 둘이 동시에 하면 틀리는 코드를 직접 만들고 고칩니다. 동시성 문제는 재현이 어려워서 어렵습니다. 그래서 이 실습은 전부 세션 두 개를 실제로 띄워 부딪히게 합니다.

환경

PostgreSQL 16 이 이미 떠 있습니다(5432, DB labdb, 사용자 lab).

export PATH=/usr/lib/postgresql/16/bin:$PATH
psql -h 127.0.0.1 -U lab -d labdb

두 세션 만드는 법

한쪽을 백그라운드로 띄우고 그동안 다른 쪽을 칩니다.

psql -h 127.0.0.1 -U lab -d labdb   -c "begin; select * from stock where id=1 for update; select pg_sleep(15);" &
sleep 1
# 여기서 두 번째 세션 작업
wait

준비

create table stock(id int primary key, qty int not null);
insert into stock values (1,10),(2,10);

단계

  1. 잠금 대기 만들기 → 01-block.txt
  2. lock_timeout02-timeout.txt
  3. 데드락 일부러 내기 → 03-deadlock.txt
  4. 순서를 맞춰 없애기 → 04-order.txt
  5. 동시 20회 차감에도 음수 없이 → 05-stock.txt
  6. skip locked 로 워커 큐 → 06-queue.txt
  7. 어드바이저리 잠금 → 07-advisory.txt
  8. 정리 → 08-notes.md

참고

3단계는 실패가 정답입니다. deadlock detected 의 DETAIL 두 줄이 누가 무엇을 기다렸는지 정확히 알려 줍니다 — 그 두 줄을 읽는 법이 이 실습에서 가장 오래 쓰이는 기술입니다.

잠금 대기를 만든다

세션 두 개로 같은 행을 for update 하세요. 두 번째가 기다리는 동안 누가 누구를 막고 있는지 찾아 01-block.txt 에 남깁니다.

먼저 create table stock(id int primary key, qty int not null); insert into stock values (1,10),(2,10);. 한쪽은 백그라운드로: psql -h 127.0.0.1 -U lab -d labdb -c "begin; select * from stock where id=1 for update; select pg_sleep(15);" &. 그다음 다른 세션에서 select pid, pg_blocking_pids(pid) from pg_stat_activity where cardinality(pg_blocking_pids(pid))>0; 를 저장하세요.

무한히 기다리지 않게 한다

lock_timeout 을 걸고 잠긴 행을 요청해 취소되는 것을 확인하세요. 그 오류 전문을 02-timeout.txt 에 남깁니다.

set lock_timeout='1s'; begin; select * from stock where id=1 for update;canceling statement due to lock timeout 이 나옵니다. 이걸 안 걸면 대기가 커넥션을 물고, 그 커넥션이 풀을 다 먹으면 DB 가 아니라 애플리케이션이 멈춥니다.

데드락을 일부러 만든다

두 트랜잭션이 서로 반대 순서로 id 1·2 를 갱신하게 해서 데드락을 내세요. deadlock detected 가 든 오류 전문을 03-deadlock.txt 에 남깁니다.

A 는 1 → (잠깐 쉬고) → 2, B 는 2 → (잠깐 쉬고) → 1. select pg_sleep(2) 를 사이에 넣어 겹치게 하세요. 둘을 백그라운드로 동시에 띄우고 wait 합니다. DETAIL 두 줄이 누가 무엇을 기다렸는지 정확히 알려 줍니다 — 그 두 줄을 읽는 법이 이 단계의 목적입니다.

순서를 맞춰 없앤다

같은 두 작업을 둘 다 id 오름차순으로 잠그게 바꿔 데드락 없이 끝나게 하세요. 두 번 이상 돌려도 실패가 없어야 합니다.

정렬 기준이 무엇이든 상관없고 모두가 같은 기준을 쓰는 것만 중요합니다. 결과를 04-order.txt 에 남기세요. 데드락은 완전히 없앨 수는 없으므로 애플리케이션은 40P01 재시도를 갖춰야 하지만, 순서를 맞추면 대부분이 사라집니다.

재고가 음수가 되지 않게

stock 의 id=1 을 10 으로 되돌린 뒤 동시에 20번 차감을 시도하세요. 재고 10 에 20번이면 최종 수량은 정확히 0 이어야 합니다. 결과를 05-stock.txt 에 남깁니다.

두 가지 중 하나면 됩니다 — for update 로 읽고 판단하거나, 한 문장으로 끝내거나(update stock set qty=qty-1 where id=1 and qty>0). 가능하면 후자가 쌉니다. 동시 실행은 for i in $(seq 20); do (psql ... &) ; done; wait.

음수가 아닌 것만으로는 통과하지 못합니다. 순진하게 select 로 읽고 그 값을 빼면 음수는 안 나오지만 결과가 9 쯤에서 멈춥니다 — 갱신이 사라진 것입니다. 그게 이 단계에서 잡으려는 버그입니다.

워커가 줄 서지 않게

jobs 테이블을 만들고 워커 둘이 서로 다른 작업을 집어 가게 하세요. 06-queue.txtworker1=... / worker2=... 두 줄로 각자 집은 id 를 남깁니다.

for update 만 쓰면 두 번째 워커가 기다립니다. for update skip locked 는 잠긴 행을 건너뜁니다. 첫 워커가 1,2 를 잡고 있으면 두 번째는 기다리지 않고 3,4 를 집습니다. 파일은 이 형식으로 — 각각 한 줄씩, 쓴 SQL 도 함께 남기세요:

worker1=1,2
worker2=3,4

행이 없는 것에 잠금을 건다

pg_try_advisory_lock 으로 같은 키를 두 세션이 동시에 잡지 못하는 것을 보이세요. 07-advisory.txtsession1= / session2= 두 줄로 결과를 남깁니다.

한 세션이 select pg_try_advisory_lock(42) 로 얻은 뒤, 다른 세션에서 같은 키를 시도하면 f 가 나옵니다(같은 세션에서 다시 하면 재진입이라 t 입니다 — 그건 증명이 안 됩니다). 파일은 이 형식으로:

session1=t
session2=f

커넥션 풀에서는 반납 전에 반드시 pg_advisory_unlock 하세요.

세 가지를 정리한다

08-notes.md 에 세 줄 이상. 데드락을 없애는 규칙 한 가지, lock_timeout 이 없으면 어디가 먼저 멈추는지, skip locked 가 바꾸는 것.

본문에 순서, 커넥션, skip locked 가 들어가야 합니다. 두 번째가 실무에서 가장 자주 오진하는 부분입니다 — 증상은 'DB 가 느리다' 가 아니라 '서버가 응답을 안 한다' 입니다.