공공부하자개발 · 영어 학습 노트
SQL
대용량·배치실행 계획·조인·인덱스·파티션·락·대량 처리0/8 완료
  • 01실행 계획 읽기
  • 02조인 방식: Nested Loop, Hash, Sort Merge 와 드라이빙 테이블
  • 03인덱스 튜닝
  • 04통계 정보와 힌트
  • 05파티션: 범위·목록·해시 분할, 파티션 프루닝, 파티션 단위 관리
  • 06대량 INSERT·UPDATE 와 배치 커밋
  • 07락과 데드락
  • 08대용량 삭제·아카이빙
사이트 소개개인정보처리방침연락처
© 2026 공부하자
홈 › 대용량·배치 › 07 / 8

락과 데드락

잠금 대기, 데드락 원인과 예방, 잠금 조회
섹션 6진행 0 / 8
1왜 배우는가2핵심 원리3코드 예제4응용 변형 예제5자주 하는 실수 (Tip)6정리‹ 이전다음 ›

3. 코드 예제

소스: sql-src/batch_07_lock_deadlock/01_lock_basics.sql, 02_job_queue.sql, 03_order_fk.sql.

이 절의 sql 은 H2 로 실행한 것입니다. 잠금이 실제로 부딪히는 장면은 세션이 하나뿐이라 만들지 못하고, 문법과 흐름만 실행으로 확인합니다.

예제 1: 수정한 행과 FOR UPDATE

자동 커밋을 끄고 김대표 행을 수정합니다. 이 시점부터 1번 행에 쓰기 잠금이 걸리고, 다른 세션이 이 행을 수정하려면 기다립니다.

sql
SET AUTOCOMMIT OFF;
UPDATE account SET balance = balance - 100 WHERE id = 1;
SELECT
       id
     , balance
  FROM account
 ORDER BY id;
text
ID | BALANCE
---+--------
1  | 900
2  | 500
3  | 300
(3행)

같은 세션은 자기가 건 잠금에 막히지 않아서 바로 값을 읽고 다시 수정할 수 있습니다. 여기서 ROLLBACK 하면 잠금이 풀리고 값이 1000 으로 돌아갑니다.

수정하지 않고 잠그기만 하려면 SELECT ... FOR UPDATE 를 씁니다. 읽은 행을 이어서 수정할 때, 그 사이에 다른 세션이 바꾸지 못하게 하려는 용도입니다.

sql
SELECT
       id
     , balance
  FROM account
 WHERE id = 2
   FOR UPDATE;
text
ID | BALANCE
---+--------
2  | 500
(1행)

남이 이 행을 잡고 있었다면 잠금이 풀릴 때까지 기다립니다. 기다리지 않으려면 뒤에 옵션을 붙입니다.

옵션 뜻 Oracle MySQL MSSQL
NOWAIT 잠겨 있으면 바로 오류 지원 8.0 부터 WITH (NOWAIT)
WAIT n 최대 n 초 기다림 지원 없음 SET LOCK_TIMEOUT
SKIP LOCKED 잠긴 행은 건너뜀 11g 부터 8.0 부터 WITH (READPAST)

H2 에서 FOR UPDATE NOWAIT 와 FOR UPDATE WAIT 2 도 문법이 통과합니다. 잡은 세션이 없어서 오류 없이 행을 돌려줄 뿐이라, 대기와 즉시 오류는 이 스크립트로 보이지 않습니다.

예제 2: 작업 큐에서 처리할 3건 잡기

job_queue 에 대기 상태 10건이 있습니다. 작업자가 여럿이면 같은 작업을 둘이 가져가지 않아야 합니다. FOR UPDATE SKIP LOCKED 는 다른 작업자가 이미 잠근 행을 건너뛰고 다음 행을 잠급니다.

sql
SET AUTOCOMMIT OFF;
SELECT
       id
  FROM job_queue
 WHERE status = 'WAIT'
 ORDER BY id
 FETCH FIRST 3 ROWS ONLY
   FOR UPDATE SKIP LOCKED;
text
ID
--
1
2
3
(3행)

작업자 w1 이 1, 2, 3번을 잡았습니다. 이 세 행은 COMMIT 할 때까지 w1 것이라, 그 사이에 w2 가 같은 문장을 실행하면 이 행들을 건너뛰고 4, 5, 6번을 가져갑니다. 이 겹침 없는 분배는 세션이 둘 있어야 보이므로 도식으로 봅니다.

시간 순서 도식(설명용). 두 작업자가 같은 문장을 실행합니다.

순서 작업자 w1 작업자 w2
1 처리할 3건 잠금 → 1, 2, 3
2 같은 문장 실행, 1~3 은 건너뜀 → 4, 5, 6
3 1~3 에 RUN 표시, COMMIT 4~6 에 RUN 표시, COMMIT

SKIP LOCKED 가 없었다면 w2 는 1번 행에서 멈춰 w1 의 COMMIT 을 기다립니다. 작업자를 늘려도 처리량이 늘지 않고 직렬로 줄을 서게 됩니다. 이제 잡은 행에 이름표를 붙이고 확정합니다.

sql
UPDATE job_queue SET status = 'RUN', picked_by = 'w1' WHERE id IN (1, 2, 3);
COMMIT;

H2 에서는 w1 이 확정한 뒤 같은 문장을 다시 실행하는 것으로 w2 의 차례를 흉내 냅니다. 대기 상태 중 남은 앞의 3건이 나옵니다.

sql
SELECT
       id
  FROM job_queue
 WHERE status = 'WAIT'
 ORDER BY id
 FETCH FIRST 3 ROWS ONLY
   FOR UPDATE SKIP LOCKED;
text
ID
--
4
5
6
(3행)

w2 도 이름표를 붙이고 COMMIT 합니다. 상태별 건수를 확인합니다.

sql
SELECT
       status
     , picked_by
     , COUNT(*) AS cnt
  FROM job_queue
 GROUP BY status, picked_by
 ORDER BY status, picked_by;
text
STATUS | PICKED_BY | CNT
-------+-----------+----
RUN    | w1        | 3
RUN    | w2        | 3
WAIT   | NULL      | 4
(3행)

작업이 끝난 w1 의 3건을 DONE 으로 바꾸고 COMMIT 하면 표가 이렇게 됩니다.

text
STATUS | CNT
-------+----
DONE   | 3
RUN    | 3
WAIT   | 4
(3행)

예제 3: 같은 순서로 잠그고 갱신하기

2.3 의 데드락은 A 가 1 → 3, B 가 3 → 1 순서로 갱신했기 때문입니다. 이체 방향과 상관없이 잠글 행을 id 순으로 먼저 잡으면 순서가 고정됩니다. 박영업(3)에서 김대표(1)로 50 을 보내는 예입니다.

sql
SET AUTOCOMMIT OFF;
SELECT
       id
     , balance
  FROM account
 WHERE id IN (3, 1)
 ORDER BY id
   FOR UPDATE;
UPDATE account SET balance = balance - 50 WHERE id = 3;
UPDATE account SET balance = balance + 50 WHERE id = 1;
COMMIT;

먼저 1번, 다음 3번 순으로 잠기고 그 뒤에 두 UPDATE 를 실행합니다. 반대 방향 이체도 같은 문장 모양이라 늘 1번을 먼저 잠급니다. 순서가 같으면 서로 상대의 행을 기다리는 순환이 만들어지지 않습니다.

시간 순서 도식(설명용). 순서를 고정하면 B 는 기다리기만 합니다.

순서 세션 A (1 → 3 이체) 세션 B (3 → 1 이체)
1 id 1, 3 을 ORDER BY id 로 잠금
2 같은 문장, 1번에서 대기
3 두 UPDATE 후 COMMIT
4 대기 해제, 잠금 후 갱신

B 는 기다릴 뿐 A 가 끝나면 이어 갑니다. 순환이 없으니 DB 가 트랜잭션을 끊을 일도 없습니다. 이 방식은 갱신할 행을 미리 알 수 있을 때 씁니다.

예제 직접 실행

아래 폴더의 SQL 파일을 Git Bash 에서 H2 메모리 DB 로 실행합니다. 방언은 파일 안의 SET MODE 로 바꿉니다.

cd sql-src/batch_07_lock_deadlock
ls *.sql
bash ../run.sh <파일>.sql
코드 예제
  • 예제 1: 수정한 행과 FOR UPDATE
  • 예제 2: 작업 큐에서 처리할 3건 잡기
  • 예제 3: 같은 순서로 잠그고 갱신하기
이전 섹션2 핵심 원리3 / 6다음 섹션4 응용 변형 예제