요약
- 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;
잠금의 동작 방식
-
배타적 잠금 (Exclusive Lock)
- 다른 트랜잭션의 SELECT FOR UPDATE 대기
- 다른 트랜잭션의 UPDATE/DELETE 대기
- 일반 SELECT는 허용 (MVCC 환경에서)
-
잠금 범위
- 인덱스를 사용한 조회: 해당 행만 잠금
- 풀 테이블 스캔: 스캔한 모든 행에 잠금 (주의!)
사용 시나리오
-- 재고 차감 예시: 레이스 컨디션 방지
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 레코드를 읽으면:
- 둘 다 queuing으로 상태 변경 시도
- 둘 다 Queue에 메시지 발송
- 중복 발행 발생
-- 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 절에 인덱스가 없으면 테이블 전체 잠금 위험
- 트랜잭션 길이: 잠금 시간이 길어지면 동시성 저하