Zettelkasten

SELECT FOR UPDATE는 행 수준 잠금을 획득한다

·수정 2026.04.24·수정 4

요약

  • SELECT FOR UPDATE는 조회한 행에 배타적 잠금(Exclusive Lock)을 걸어 다른 트랜잭션의 수정을 막는다
  • 동시성 제어가 필요한 상황에서 레이스 컨디션을 방지하는 비관적 잠금(Pessimistic Lock) 기법이다

본문

기본 원리

SELECT FOR UPDATE는 SELECT 쿼리 실행 시 해당 행에 **배타적 잠금(X-Lock)**을 건다. 이 잠금은 트랜잭션이 커밋되거나 롤백될 때까지 유지된다.

-- 트랜잭션 A
BEGIN;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
-- 이 시점에 id=1 행에 잠금 획득
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;

잠금의 동작 방식

  1. 배타적 잠금 (Exclusive Lock)

    • 다른 트랜잭션의 SELECT FOR UPDATE 대기
    • 다른 트랜잭션의 UPDATE/DELETE 대기
    • 일반 SELECT는 허용 (MVCC 환경에서)
  2. 잠금 범위

    • 인덱스를 사용한 조회: 해당 행만 잠금
    • 풀 테이블 스캔: 스캔한 모든 행에 잠금 (주의!)

사용 시나리오

-- 재고 차감 예시: 레이스 컨디션 방지
BEGIN;
SELECT quantity FROM products WHERE id = 100 FOR UPDATE;
-- quantity가 충분한지 확인 후
UPDATE products SET quantity = quantity - 1 WHERE id = 100;
COMMIT;

Outbox Poller에서의 필수 사용

메시지 발송 시 SELECT FOR UPDATE는 필수다. 여러 Poller 인스턴스가 동시에 같은 pending 레코드를 읽으면:

  1. 둘 다 queuing으로 상태 변경 시도
  2. 둘 다 Queue에 메시지 발송
  3. 중복 발행 발생
-- Outbox Poller 예시: SKIP LOCKED로 경합 없이 분산 처리
BEGIN;
SELECT * FROM outbox
WHERE status = 'PENDING'
ORDER BY created_at
LIMIT 100
FOR UPDATE SKIP LOCKED;

-- 상태 변경 후 Queue 발송
UPDATE outbox SET status = 'QUEUING' WHERE id IN (...);
COMMIT;
-- Queue.send() 실행

SKIP LOCKED 옵션으로 이미 다른 Poller가 잡은 행은 건너뛰어 경합 없이 병렬 처리 가능.

옵션들

옵션 설명
FOR UPDATE 배타적 잠금, 대기
FOR UPDATE NOWAIT 잠금 불가 시 즉시 에러
FOR UPDATE SKIP LOCKED 잠금된 행 건너뛰기 (Poller에 적합)

주의사항

  • 데드락 가능성: 여러 행을 순서 없이 잠그면 데드락 발생 가능
  • 인덱스 필수: WHERE 절에 인덱스가 없으면 테이블 전체 잠금 위험
  • 트랜잭션 길이: 잠금 시간이 길어지면 동시성 저하

참고