본문으로 건너뛰기
3.5 어드바이저리 락

3.5 어드바이저리 락

PostgreSQL의 advisory lock은 SQL 객체에 묶이지 않은 임의의 정수 키 락입니다. 데이터 변경 없이 애플리케이션 레벨에서 동시성 제어가 필요할 때 사용합니다. 분산 락 매니저(Redis·ZooKeeper)를 따로 두지 않고 DB 한 곳에서 처리하고 싶을 때 강력합니다.

키 형태

advisory lock은 두 가지 키 형태를 받습니다.

형식
pg_advisory_lock(bigint)64-bit 정수 1개
pg_advisory_lock(int4, int4)32-bit 정수 2개 (classid + objid 흉내)

같은 키는 같은 락. 키 공간은 클러스터 전역이다 — 모든 데이터베이스가 같은 키 공간을 공유합니다. 충돌을 피하려면 (application_id, resource_id) 같은 형태로 둘로 나눠 쓰는 게 안전합니다.

4가지 변종

함수트랜잭션 종료 시 자동 해제블로킹공유/배타
pg_advisory_lock(key)아니오 — 세션 종료까지대기배타
pg_advisory_xact_lock(key)대기배타
pg_try_advisory_lock(key)아니오즉시 반환 (true/false)배타
pg_try_advisory_xact_lock(key)즉시 반환배타

각각 _shared 접미사로 공유 락 버전이 있다 — pg_advisory_lock_shared, pg_try_advisory_xact_lock_shared 등.

배타 락은 공유 락과 충돌하지만, 공유 락 여럿은 같이 잡힐 수 있습니다.

두 가지 큰 패턴

1. 트랜잭션 단위 락 (xact 변종) — 권장

BEGIN;
-- 동시에 한 사용자에 대해서만 결제 처리
SELECT pg_advisory_xact_lock(hashtext('user_payment'), 42);

-- 잔액 차감
UPDATE accounts SET balance = balance - 100 WHERE user_id = 42;

COMMIT;
-- COMMIT 시 락 자동 해제

pg_advisory_xact_lock트랜잭션이 끝나면 무조건 해제되므로 누수 위험이 가장 적습니다. 일상적으로는 이 변종만 써도 충분합니다.

2. 세션 단위 락 — 장기 점유

-- 워커 1이 백그라운드 작업 시작
SELECT pg_try_advisory_lock(1, 1) AS got_lock;
--  got_lock
-- ----------
--  t

-- 다른 워커가 시도하면
SELECT pg_try_advisory_lock(1, 1);
--  pg_try_advisory_lock
-- ----------------------
--  f

-- 작업 끝나면 직접 해제
SELECT pg_advisory_unlock(1, 1);

세션 단위 락은 세션이 끊기면 해제됩니다. 워커가 죽으면 다른 워커가 인계받을 수 있어 분산 작업 분배에 유리.

자주 쓰는 시나리오

시나리오권장 변종
결제·재고 등 row 단위 직렬화pg_advisory_xact_lock
크론 잡 같은 시간 단위로 하나만 실행pg_try_advisory_lock (세션 단위)
메시지 큐의 메시지 1개 처리SELECT ... FOR UPDATE SKIP LOCKED이 더 적합
배포 시 마이그레이션 직렬화pg_try_advisory_lock
분산 시스템의 leader electionpg_try_advisory_lock + heartbeat
외부 시스템 동기화 (한 번에 하나)pg_try_advisory_xact_lock

흐름 예시 — 크론 잡 단일 실행

여러 인스턴스가 같은 크론 잡을 1분마다 실행하지만, 한 번에 하나만 돌게 하고 싶습니다.

def run_daily_job():
    with conn.cursor() as cur:
        cur.execute("SELECT pg_try_advisory_lock(%s, %s)", (1001, 1))
        got = cur.fetchone()[0]
        if not got:
            log.info("이미 다른 인스턴스가 실행 중 — 종료")
            return
        try:
            do_work()
        finally:
            cur.execute("SELECT pg_advisory_unlock(%s, %s)", (1001, 1))

만약 인스턴스가 죽으면 PostgreSQL이 세션 종료를 감지해 자동으로 락을 해제하므로, 다음 분에 다른 인스턴스가 가져가 진행합니다.

진단 — 누가 잡고 있나

pg_locks에 advisory lock도 노출됩니다.

SELECT locktype, classid, objid, mode, granted, pid
  FROM pg_locks
 WHERE locktype = 'advisory'
 ORDER BY pid;
컬럼의미
classid첫 번째 키 (또는 64-bit 키의 상위 32-bit)
objid두 번째 키 (또는 64-bit 키의 하위 32-bit)
modeExclusiveLock 또는 ShareLock
granted잡혔는지 대기인지

키 충돌 회피

advisory lock의 키 공간은 데이터베이스 경계를 넘어 클러스터 전체에서 공유됩니다. 다른 애플리케이션·다른 라이브러리가 같은 키를 우연히 쓰면 사고입니다.

권장 패턴:

-- 애플리케이션 ID를 상위 키에, 리소스 ID를 하위에
SELECT pg_advisory_xact_lock(
  hashtext('myapp.payment')::int,   -- 상위 32-bit
  user_id                            -- 하위 32-bit
);

hashtext()는 문자열을 32-bit 정수로 해시. 다른 앱과의 충돌 확률을 낮춥니다. 단, 해시 충돌은 0이 아니므로 매우 중요한 경우 명시적 ID 매핑이 더 안전합니다.

advisory lock vs row lock

측면SELECT ... FOR UPDATEadvisory lock
락 대상실제 row임의의 정수 키
데이터 변경 필요row가 존재해야 함없어도 됨
락 해제트랜잭션 종료트랜잭션 또는 세션 종료 (변종 선택)
격리 효과DB 일관성 일부애플리케이션 로직 직렬화
모니터링pg_locks.locktype='tuple'/'relation'pg_locks.locktype='advisory'

선택 기준: row가 있고 그 row에 결제·갱신을 하려면 FOR UPDATE. row가 없거나 외부 자원과의 동기화면 advisory lock.

운영 시 주의

주의메모
세션 단위 락 누수워커가 락 잡고 죽으면 OK (세션 종료로 자동 해제). 그러나 connection pool에서 같은 세션을 재사용하면 락이 살아 있을 수 있음
pg_advisory_unlock_all()현재 세션이 잡은 advisory lock 전체 해제 — pool 반환 직전 안전망으로 유용
idle_in_transaction_session_timeoutxact 변종은 트랜잭션이 끊기면 락도 풀린다 — 안전
워크로드 분석advisory lock 충돌 횟수는 pg_stat_activity.wait_event='advisory'로 관측
connection pooler(pgBouncer)와 session lock: pgBouncer transaction 모드에서는 한 클라이언트의 세션이 여러 백엔드로 분산됩니다. 세션 단위 advisory lock을 잡으면 다음 호출에서 다른 백엔드를 받아 락이 안 풀린 상태가 됩니다. pgBouncer 뒤에서는 pg_advisory_xact_lock 쓰는 게 안전합니다.

정리

  • advisory lock은 임의의 정수 키로 잡는 락 — 데이터 변경 없이 동시성 제어
  • 4가지 변종: 세션/트랜잭션 × 블로킹/논블로킹. xact 변종이 가장 안전
  • 키 공간은 클러스터 전역 — 충돌 피하려면 (app_id, resource_id) 형태
  • 크론 잡 단일 실행, 마이그레이션 직렬화, 외부 시스템 동기화 등에 표준
  • row lock으로 대신 가능한 경우는 row lock이 더 명확하다 — FOR UPDATE 우선 검토
  • pgBouncer transaction 모드 뒤에서는 xact 변종만 안전

Part III 트랜잭션과 동시성이 끝났습니다. 다음 Part IV에서는 디스크 I/O를 흡수하는 버퍼 매니저와 WAL을 봅니다.