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 election | pg_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) |
mode | ExclusiveLock 또는 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 UPDATE | advisory 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_timeout | xact 변종은 트랜잭션이 끊기면 락도 풀린다 — 안전 |
| 워크로드 분석 | advisory lock 충돌 횟수는 pg_stat_activity.wait_event='advisory'로 관측 |
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을 봅니다.