잠금과 동시성 (Locking and Concurrency)
잠금과 동시성 (Locking and Concurrency)
명시적 잠금 (Explicit Locking)
PostgreSQL은 테이블에 대한 동시 접근을 제어하기 위해 여러 가지 잠금 모드를 제공해요. 이런 모드는 MVCC가 원하는 대로 동작하지 않는 상황에서 애플리케이션이 직접 잠금을 제어할 때 쓸 수 있어요. 또, 대부분의 PostgreSQL 명령은 명령이 실행되는 동안 참조된 테이블이 drop되거나 호환되지 않는 방식으로 수정되지 않도록, 적절한 모드의 잠금을 자동으로 획득해요. (예를 들어 TRUNCATE는 같은 테이블에 대한 다른 연산과 동시에 안전하게 실행될 수 없기 때문에, 이를 강제하기 위해 테이블에 ACCESS EXCLUSIVE 잠금을 획득해요.)
데이터베이스 서버에서 현재 걸려 있는 잠금의 목록을 확인하려면 pg_locks 시스템 뷰를 사용하면 돼요. 잠금 관리자 하위 시스템의 상태를 모니터링하는 방법에 대한 자세한 내용은 27장을 참고해요.
테이블 수준 잠금 (Table-Level Locks)
아래 목록은 사용할 수 있는 잠금 모드와 PostgreSQL이 자동으로 사용하는 상황을 보여줘요. 이런 잠금은 LOCK 명령으로 명시적으로 획득할 수도 있어요. 주의할 점이 하나 있는데, 이름에 "row"라는 단어가 들어가 있더라도 이 모든 잠금 모드는 테이블 수준 잠금이에요. 잠금 모드의 이름은 역사적인 이유로 붙은 거예요. 어느 정도 이름이 각 잠금 모드의 전형적인 용도를 반영하긴 하지만, 의미는 모두 같아요. 잠금 모드 사이의 실제 차이는 각 잠금 모드가 어떤 모드와 충돌하는지뿐이에요 (표 13.2 참고). 두 트랜잭션이 같은 테이블에서 동시에 충돌하는 모드의 잠금을 보유할 수는 없어요. (다만 한 트랜잭션이 자기 자신과 충돌하는 일은 없어요. 예를 들어 같은 테이블에서 ACCESS EXCLUSIVE 잠금을 획득한 뒤 나중에 ACCESS SHARE 잠금을 획득할 수 있어요.) 충돌하지 않는 잠금 모드는 많은 트랜잭션이 동시에 보유할 수 있어요. 특히 어떤 잠금 모드는 자기 자신과도 충돌하는데(예: ACCESS EXCLUSIVE 잠금은 한 번에 둘 이상의 트랜잭션이 보유할 수 없음), 어떤 모드는 그렇지 않다는 점(예: ACCESS SHARE 잠금은 여러 트랜잭션이 보유 가능)을 눈여겨봐야 해요.
테이블 수준 잠금 모드
-
ACCESS SHARE (
AccessShareLock):ACCESS EXCLUSIVE잠금 모드하고만 충돌해요.SELECT명령이 참조하는 테이블에서 이 모드의 잠금을 획득해요. 일반적으로 테이블을 읽고 수정하지 않는 모든 쿼리가 이 잠금 모드를 획득해요. -
ROW SHARE (
RowShareLock):EXCLUSIVE및ACCESS EXCLUSIVE잠금 모드와 충돌해요.SELECT명령에서FOR UPDATE,FOR NO KEY UPDATE,FOR SHARE,FOR KEY SHARE옵션 중 하나가 지정된 모든 테이블에서 이 잠금 모드를 획득해요 (명시적인FOR ...잠금 옵션 없이 참조되는 다른 테이블에는ACCESS SHARE잠금을 추가로 획득해요). -
ROW EXCLUSIVE (
RowExclusiveLock):SHARE,SHARE ROW EXCLUSIVE,EXCLUSIVE,ACCESS EXCLUSIVE잠금 모드와 충돌해요.UPDATE,DELETE,INSERT,MERGE명령이 대상 테이블에서 이 잠금 모드를 획득해요 (다른 참조 테이블에는ACCESS SHARE잠금을 추가로 획득해요). 일반적으로 테이블의 데이터를 수정하는 모든 명령이 이 잠금 모드를 획득해요. -
SHARE UPDATE EXCLUSIVE (
ShareUpdateExclusiveLock):SHARE UPDATE EXCLUSIVE,SHARE,SHARE ROW EXCLUSIVE,EXCLUSIVE,ACCESS EXCLUSIVE잠금 모드와 충돌해요. 이 모드는 테이블을 동시 스키마 변경과 VACUUM 실행으로부터 보호해요.VACUUM(FULL 없이),ANALYZE,CREATE INDEX CONCURRENTLY,CREATE STATISTICS,COMMENT ON,REINDEX CONCURRENTLY, 그리고 특정ALTER INDEX와ALTER TABLE변형(자세한 내용은 각 명령의 문서 참고)이 획득해요. -
SHARE (
ShareLock):ROW EXCLUSIVE,SHARE UPDATE EXCLUSIVE,SHARE ROW EXCLUSIVE,EXCLUSIVE,ACCESS EXCLUSIVE잠금 모드와 충돌해요. 이 모드는 테이블을 동시 데이터 변경으로부터 보호해요.CREATE INDEX(CONCURRENTLY 없이)가 획득해요. -
SHARE ROW EXCLUSIVE (
ShareRowExclusiveLock):ROW EXCLUSIVE,SHARE UPDATE EXCLUSIVE,SHARE,SHARE ROW EXCLUSIVE,EXCLUSIVE,ACCESS EXCLUSIVE잠금 모드와 충돌해요. 이 모드는 테이블을 동시 데이터 변경으로부터 보호하고, 자기 자신과도 배타적이라서 한 번에 하나의 세션만 보유할 수 있어요.CREATE TRIGGER와 일부 형태의ALTER TABLE이 획득해요. -
EXCLUSIVE (
ExclusiveLock):ROW SHARE,ROW EXCLUSIVE,SHARE UPDATE EXCLUSIVE,SHARE,SHARE ROW EXCLUSIVE,EXCLUSIVE,ACCESS EXCLUSIVE잠금 모드와 충돌해요. 이 모드는ACCESS SHARE잠금만 동시에 허용해요. 즉, 이 잠금 모드를 보유한 트랜잭션과 병렬로는 테이블 읽기만 진행할 수 있어요.REFRESH MATERIALIZED VIEW CONCURRENTLY가 획득해요. -
ACCESS EXCLUSIVE (
AccessExclusiveLock): 모든 모드의 잠금(ACCESS SHARE,ROW SHARE,ROW EXCLUSIVE,SHARE UPDATE EXCLUSIVE,SHARE,SHARE ROW EXCLUSIVE,EXCLUSIVE,ACCESS EXCLUSIVE)과 충돌해요. 이 모드는 보유자가 어떤 방식으로든 테이블에 접근하는 유일한 트랜잭션이라는 것을 보장해요.DROP TABLE,TRUNCATE,REINDEX,CLUSTER,VACUUM FULL,REFRESH MATERIALIZED VIEW(CONCURRENTLY 없이) 명령이 획득해요. 많은 형태의ALTER INDEX와ALTER TABLE도 이 수준의 잠금을 획득해요. 이는 또한 모드를 명시적으로 지정하지 않는LOCK TABLE문의 기본 잠금 모드이기도 해요.
팁: 오직
ACCESS EXCLUSIVE잠금만SELECT(FOR UPDATE/SHARE 없이)를 막아요.
잠금은 일단 획득되면 일반적으로 트랜잭션 끝까지 보유돼요. 하지만 savepoint를 설정한 뒤 잠금을 획득했다면, savepoint로 롤백했을 때 잠금이 즉시 해제돼요. 이는 ROLLBACK이 savepoint 이후의 모든 명령 효과를 취소한다는 원칙과 일치해요. PL/pgSQL 예외 블록 안에서 획득한 잠금도 마찬가지예요. 블록에서 오류로 빠져나오면 그 안에서 획득한 잠금이 해제돼요.
표 13.2. 충돌하는 잠금 모드
| 요청된 잠금 모드 | ACCESS SHARE | ROW SHARE | ROW EXCL. | SHARE UPDATE EXCL. | SHARE | SHARE ROW EXCL. | EXCL. | ACCESS EXCL. |
|---|---|---|---|---|---|---|---|---|
| ACCESS SHARE | X | |||||||
| ROW SHARE | X | X | ||||||
| ROW EXCL. | X | X | X | |||||
| SHARE UPDATE EXCL. | X | X | X | X | X | |||
| SHARE | X | X | X | X | X | |||
| SHARE ROW EXCL. | X | X | X | X | X | X | ||
| EXCL. | X | X | X | X | X | X | X | |
| ACCESS EXCL. | X | X | X | X | X | X | X | X |
행 수준 잠금 (Row-Level Locks)
테이블 수준 잠금과 더불어 행 수준 잠금도 있어요. 아래는 PostgreSQL이 자동으로 사용하는 상황과 함께 나열한 행 수준 잠금이에요. 행 수준 잠금 충돌의 전체 표는 표 13.3을 참고해요. 트랜잭션은 서로 다른 하위 트랜잭션에 있더라도 같은 행에서 충돌하는 잠금을 보유할 수 있지만, 그 외에 두 트랜잭션이 같은 행에서 충돌하는 잠금을 동시에 보유할 수는 없어요. 행 수준 잠금은 데이터 조회에는 영향을 주지 않아요. 같은 행에 대한 작성자와 잠금 획득자만 막아요. 행 수준 잠금은 테이블 수준 잠금과 마찬가지로 트랜잭션 종료 시 또는 savepoint 롤백 중에 해제돼요.
행 수준 잠금 모드
-
FOR UPDATE:
SELECT문이 가져온 행을 업데이트하듯 잠궈요. 이는 현재 트랜잭션이 끝날 때까지 다른 트랜잭션이 그 행을 잠그거나, 수정하거나, 삭제하지 못하게 해요. 즉, 그 행에 대해UPDATE,DELETE,SELECT FOR UPDATE,SELECT FOR NO KEY UPDATE,SELECT FOR SHARE,SELECT FOR KEY SHARE를 시도하는 다른 트랜잭션은 현재 트랜잭션이 끝날 때까지 차단돼요. 반대로SELECT FOR UPDATE는 같은 행에서 그런 명령 중 하나를 실행한 동시 트랜잭션을 기다렸다가, 업데이트된 행을 잠그고 반환해요(행이 삭제되었다면 행이 없을 수도 있어요). 하지만REPEATABLE READ또는SERIALIZABLE트랜잭션 안에서는, 잠글 행이 트랜잭션 시작 이후 변경되었다면 오류가 발생해요. 자세한 논의는 13.4절을 참고해요.FOR UPDATE잠금 모드는 행에 대한 어떤DELETE와, 특정 열의 값을 수정하는UPDATE에서도 획득해요. 현재UPDATE의 경우로 간주되는 열 집합은 외래 키에 사용될 수 있는 고유 인덱스가 있는 열들이에요(부분 인덱스와 표현식 인덱스는 고려되지 않아요). 다만 이는 나중에 바뀔 수 있어요. -
FOR NO KEY UPDATE:
FOR UPDATE와 비슷하게 동작하지만, 획득하는 잠금이 더 약해요. 이 잠금은 같은 행에 대해 잠금을 획득하려는SELECT FOR KEY SHARE명령을 막지 않아요. 이 잠금 모드는FOR UPDATE잠금을 획득하지 않는 모든UPDATE에서도 획득돼요. -
FOR SHARE:
FOR NO KEY UPDATE와 비슷하게 동작하지만, 각 가져온 행에 대해 배타적 잠금 대신 공유 잠금을 획득해요. 공유 잠금은 다른 트랜잭션이 이 행에 대해UPDATE,DELETE,SELECT FOR UPDATE,SELECT FOR NO KEY UPDATE를 수행하는 것을 막지만,SELECT FOR SHARE나SELECT FOR KEY SHARE를 수행하는 것은 막지 않아요. -
FOR KEY SHARE:
FOR SHARE와 비슷하게 동작하지만, 잠금이 더 약해요.SELECT FOR UPDATE는 막히지만SELECT FOR NO KEY UPDATE는 막히지 않아요. 키 공유 잠금은 다른 트랜잭션이DELETE나 키 값을 변경하는 어떤UPDATE를 수행하는 것을 막지만, 다른UPDATE는 막지 않아요.SELECT FOR NO KEY UPDATE,SELECT FOR SHARE,SELECT FOR KEY SHARE도 막지 않아요.
PostgreSQL은 수정된 행에 대한 어떤 정보도 메모리에 기억하지 않기 때문에, 한 번에 잠글 수 있는 행 수에는 제한이 없어요. 다만 행을 잠그면 디스크 쓰기가 발생할 수 있어요. 예를 들어 SELECT FOR UPDATE는 잠금 표시를 위해 선택된 행을 수정하므로 디스크 쓰기가 발생해요.
표 13.3. 충돌하는 행 수준 잠금
| 요청된 잠금 모드 | 현재 잠금 모드 | |||
|---|---|---|---|---|
| FOR KEY SHARE | FOR SHARE | FOR NO KEY UPDATE | FOR UPDATE | |
| FOR KEY SHARE | X | |||
| FOR SHARE | X | X | ||
| FOR NO KEY UPDATE | X | X | X | |
| FOR UPDATE | X | X | X | X |
페이지 수준 잠금 (Page-Level Locks)
테이블 및 행 잠금 외에도, 공유 버퍼 풀의 테이블 페이지에 대한 읽기/쓰기 접근을 제어하기 위해 페이지 수준 공유/배타 잠금이 사용돼요. 이 잠금은 행을 가져오거나 업데이트한 직후 해제돼요. 애플리케이션 개발자는 보통 페이지 수준 잠금에 신경 쓸 필요가 없지만, 완전성을 위해 여기에서 언급해요.
교착 상태 (Deadlocks)
명시적 잠금을 사용하면 교착 상태의 가능성이 높아질 수 있어요. 교착 상태란 두 개(또는 그 이상)의 트랜잭션이 서로 상대방이 원하는 잠금을 각자 보유한 상태를 말해요. 예를 들어 트랜잭션 1이 테이블 A에 배타적 잠금을 획득한 뒤 테이블 B에 배타적 잠금을 획득하려 하는데, 트랜잭션 2가 이미 테이블 B를 배타적으로 잠갔고 이제 테이블 A에 배타적 잠금을 원한다면, 둘 다 진행할 수 없어요. PostgreSQL은 교착 상태를 자동으로 감지하고, 관련된 트랜잭션 중 하나를 중단(abort)시켜 나머지가 완료되도록 해결해요. (정확히 어떤 트랜잭션이 중단될지는 예측하기 어렵고, 이에 의존해서는 안 돼요.)
교착 상태는 행 수준 잠금의 결과로도 발생할 수 있어요(그래서 명시적 잠금을 사용하지 않아도 발생할 수 있어요). 두 동시 트랜잭션이 테이블을 수정하는 경우를 생각해보죠. 첫 번째 트랜잭션이 다음을 실행해요:
UPDATE accounts SET balance = balance + 100.00 WHERE acctnum = 11111;
이것은 지정된 계좌 번호의 행에 행 수준 잠금을 획득해요. 그다음 두 번째 트랜잭션이 다음을 실행해요:
UPDATE accounts SET balance = balance + 100.00 WHERE acctnum = 22222;
UPDATE accounts SET balance = balance - 100.00 WHERE acctnum = 11111;
첫 번째 UPDATE 문은 지정된 행에 행 수준 잠금을 성공적으로 획득하므로 그 행을 업데이트해요. 그런데 두 번째 UPDATE 문은 업데이트하려는 행이 이미 잠겨 있다는 것을 발견하고, 잠금을 획득한 트랜잭션이 완료되기를 기다려요. 이제 트랜잭션 2는 계속 실행되기 전에 트랜잭션 1이 완료되기를 기다리고 있어요. 이제 트랜잭션 1이 다음을 실행해요:
UPDATE accounts SET balance = balance - 100.00 WHERE acctnum = 22222;
트랜잭션 1은 지정된 행에 행 수준 잠금을 획득하려 하지만, 트랜잭션 2가 이미 그 잠금을 보유하고 있어서 획득할 수 없어요. 그래서 트랜잭션 2가 완료되기를 기다려요. 결과적으로 트랜잭션 1은 트랜잭션 2에 막히고, 트랜잭션 2는 트랜잭션 1에 막히는 교착 상태가 돼요. PostgreSQL은 이 상황을 감지하고 두 트랜잭션 중 하나를 중단해요.
교착 상태에 대한 가장 좋은 방어책은 일반적으로, 데이터베이스를 사용하는 모든 애플리케이션이 여러 객체에 대한 잠금을 일관된 순서로 획득하도록 확실히 해서 교착 상태를 피하는 거예요. 위 예시에서 두 트랜잭션이 행을 같은 순서로 업데이트했다면 교착 상태가 발생하지 않았을 거예요. 또한 트랜잭션에서 객체에 처음 획득하는 잠금이 그 객체에 필요할 가장 제한적인 모드가 되도록 확실히 해야 해요. 이것을 사전에 검증하는 것이 불가능하다면, 교착 상태로 인해 중단된 트랜잭션을 재시도하는 방식으로 즉석에서 처리할 수 있어요.
교착 상태 상황이 감지되지 않는 한, 테이블 수준 또는 행 수준 잠금을 찾는 트랜잭션은 충돌하는 잠금이 해제될 때까지 무기한 기다려요. 이는 애플리케이션이 오랫동안(예: 사용자 입력을 기다리는 동안) 트랜잭션을 열어 두는 것은 좋지 않은 생각이라는 뜻이에요.
Advisory 잠금 (Advisory Locks)
PostgreSQL은 애플리케이션이 정의한 의미를 가진 잠금을 만드는 수단을 제공해요. 이를 advisory lock이라고 하는데, 시스템이 그 사용을 강제하지 않기 때문이에요. 제대로 사용하는 것은 애플리케이션의 몫이에요. Advisory 잠금은 MVCC 모델에 잘 맞지 않는 잠금 전략에 유용할 수 있어요. 예를 들어 advisory 잠금의 일반적인 용도는 소위 "플랫 파일" 데이터 관리 시스템의 전형인 비관적 잠금 전략을 흉내 내는 거예요. 테이블에 저장된 플래그가 같은 목적으로 사용될 수 있지만, advisory 잠금은 더 빠르고 테이블 블로트(bloat)를 피하며 세션이 끝날 때 서버가 자동으로 정리해줘요.
PostgreSQL에서 advisory 잠금을 획득하는 방법은 두 가지가 있어요. 세션 수준 또는 트랜잭션 수준이에요. 세션 수준에서 획득하면 advisory 잠금은 명시적으로 해제하거나 세션이 끝날 때까지 보유돼요. 표준 잠금 요청과 달리 세션 수준 advisory 잠금 요청은 트랜잭션 의미를 따르지 않아요. 나중에 롤백된 트랜잭션 중에 획득한 잠금은 롤백 뒤에도 여전히 보유되고, 마찬가지로 호출 트랜잭션이 나중에 실패하더라도 잠금 해제는 유효해요. 잠금은 소유 프로세스가 여러 번 획득할 수 있어요. 각 완료된 잠금 요청에 대해 잠금이 실제로 해제되기 전에 대응하는 잠금 해제 요청이 있어야 해요. 반면 트랜잭션 수준 잠금 요청은 일반 잠금 요청처럼 동작해요. 트랜잭션 끝에 자동으로 해제되고 명시적인 잠금 해제 연산은 없어요. 이 동작은 advisory 잠금을 단기간 사용할 때 세션 수준 동작보다 편리한 경우가 많아요. 같은 advisory 잠금 식별자에 대한 세션 수준과 트랜잭션 수준 잠금 요청은 예상한 방식으로 서로를 막아요. 세션이 특정 advisory 잠금을 이미 보유하고 있다면, 다른 세션이 그 잠금을 기다리고 있어도 추가 요청은 항상 성공해요. 이 말은 기존 잠금 보유와 새 요청이 세션 수준이든 트랜잭션 수준이든 관계없이 참이에요.
PostgreSQL의 모든 잠금과 마찬가지로, 어떤 세션이 현재 보유한 advisory 잠금의 전체 목록은 pg_locks 시스템 뷰에서 찾을 수 있어요.
Advisory 잠금과 일반 잠금 모두 공유 메모리 풀에 저장되는데, 그 크기는 구성 변수 max_locks_per_transaction와 max_connections로 정의돼요. 이 메모리를 소진하지 않도록 주의해야 해요. 소진하면 서버가 어떤 잠금도 부여할 수 없게 되거든요. 이는 서버가 부여할 수 있는 advisory 잠금 수에 상한을 두는데, 서버 구성 방식에 따라 보통 수만 개에서 수십만 개 사이예요.
특정 경우에, 특히 명시적 정렬과 LIMIT 절을 포함하는 쿼리에서 advisory 잠금 방법을 사용할 때는 SQL 표현식이 평가되는 순서 때문에 획득되는 잠금을 제어하는 데 주의해야 해요. 예를 들어:
SELECT pg_advisory_lock(id) FROM foo WHERE id = 12345; -- ok
SELECT pg_advisory_lock(id) FROM foo WHERE id > 12345 LIMIT 100; -- danger!
SELECT pg_advisory_lock(q.id) FROM
(
SELECT id FROM foo WHERE id > 12345 LIMIT 100
) q; -- ok
위 쿼리에서 두 번째 형태는 LIMIT가 잠금 함수가 실행되기 전에 적용된다는 것이 보장되지 않으므로 위험해요. 이로 인해 애플리케이션이 기대하지 않은 잠금이 획득될 수 있고, 그래서 (세션이 끝날 때까지) 해제에 실패할 수 있어요. 애플리케이션의 관점에서 그런 잠금은 여전히 pg_locks에서 볼 수 있기는 하지만 매달린(dangling) 잠금이 돼요.
Advisory 잠금을 조작하기 위해 제공되는 함수는 9.28.10절에 설명돼 있어요.
출처: PostgreSQL 18 공식 문서 — 13.3. Explicit Locking (https://www.postgresql.org/docs/current/explicit-locking.html). 원문은 https://www.postgresql.org/docs/current/locking.html 로 요청 시 404 페이지(문서 재구성으로 파일명 변경)이므로, 내용이 동일한 현재 페이지를 사용했어요.