카테고리 없음

PostgreSQL 동시성 제어

남용2 2019. 11. 22. 15:14

PostgreSQL은 동시성 제어에 대해 관계형 데이터를 처리할 수 있다.

동시성 제어는 SQL 동작 방식을 이해해야 한다.

동시성 제어는 PostgreSQL의 공식 문서인 https://www.postgresql.org/docs/current/static/mvcc.html에서 더 자세한 정보를 얻을 수 있다.

동시성 제어

다중버전동시제어(MultiVersion Concurrency Control: MVCC)

1999년 Vadim이 PostgreSQL 6.5부터 MVCC 아키텍처를 도입했다.

 

트랜잭션(Transaction)

트랜잭션은 앞뒤에 BEGIN 및 COMMIT 명령을 사용한 SQL 명령으로 설정된다.

COMMIT 대신 ROLLBACK를 사용하면 지금까지 수행한 SQL 명령이 취소된다.

모든 SQL 명령은 트랜잭션 내에서 실행되는 것으로 취급된다.

BEGIN 명령이 없을 경우 SQL 문의 앞뒤에 암시적으로 BEGIN 및 COMMIT 명령이 추가된다.

BEGIN 및 COMMIT이 사용된 명령을 트랜잭션 블록이라고 한다.

SAVEPOINT를 사용해 특정 트랜잭션 지점을 정의할 수 있고, ROLLBACK TO를 사용해 특정 트랜잭션 지점까지 ROLLBACK할 수 있다.

 

트랜잭션 격리(Transaction Isolation) 

SQL 표준은 Transaction Isolation에 대해 4가지 레벨로 정의하고 있다. 이 4가지 중 가장 엄격한 레벨은 Serializable이다.

PostgreSQL은 3가지 레벨을 지원하며, Read committed, Repeatable read, Serializable이다. 3가지만 지원하는 이유는 MVCC 모델에 적용할 수 있는 합리적인 방법이기 때문이다.

PostgreSQL의 일부 자료형과 함수는 트랜잭션 내 특별한 형태로 읽기 특성을 제공한다. 예를 들어  자동 증가 컬럼으로 사용되는 serial 자료형과 sequence같은 객체는 rollback이 없으며, 자료 변경 즉시 다른 세션에서도 그 변경된 값을 볼 수 있다.

트랜잭션 격리 레벨을 사용자가 지정하려면 SET TRANSACTION 명령(https://www.postgresql.org/docs/10/static/sql-set-transaction.html)을 사용한다. 

SET TRANSACTION 명령어는 현재 트랜잭션의 특성을 설정한다. 문법은 다음과 같다.

Synopsis

SET TRANSACTION transaction_mode [, ...]

SET TRANSACTION SNAPSHOT snapshot_id

SET SESSION CHARACTERISTICS AS TRANSACTION transaction_mode [, ...]

 

where transaction_mode is one of:

 

    ISOLATION LEVEL { SERIALIZABLE | REPEATABLE READ | READ COMMITTED | READ UNCOMMITTED }

    READ WRITE | READ ONLY

    [ NOT ] DEFERRABLE

SET TRANSACTION transaction_mode 명령은 BEGIN 및 COMMIT 명령으로 된 트랜잭션 블록에서만 사용할 수 있다. 다음은 예제이다.

postgres=# BEGIN;

BEGIN

postgres=# SHOW TRANSACTION ISOLATION LEVEL;

 transaction_isolation

-----------------------

 read committed

(1 row)

 

postgres=# SET TRANSACTION ISOLATION LEVEL serializable;

SET

postgres=# SHOW TRANSACTION ISOLATION LEVEL;

 transaction_isolation

-----------------------

 serializable

(1 row)

 

postgres=# COMMIT;

COMMIT

SET TRANSACTION SNAPSHOT snapshot_id 명령은 특정 snapshot으로 트랜잭션을 시작할 수 있다. 다음은 예제이다.

--세션 1

[bylee@bylee5 ~]$ agens -d postgres

agens (AgensGraph 1.4devel, based on PostgreSQL 10.3)

Type "help" for help.

 

postgres=# begin;

BEGIN

postgres=# select pg_export_snapshot();

 pg_export_snapshot 

---------------------

 00000005-000000FE-1

(1 row)

 

--세션 2

[bylee@bylee5 ~]$ agens -d postgres

agens (AgensGraph 1.4devel, based on PostgreSQL 10.3)

Type "help" for help.

 

postgres=# begin isolation level repeatable read;

BEGIN

postgres=# set transaction snapshot '00000005-000000FE-1';

SET

 

SET SESSION CHARACTERISTICS AS TRANSACTION transaction_mode 명령은 세션의 기본 트랜잭션 격리 레벨을 설정할 수 있다. 다음은 예제이다.

postgres=# SHOW DEFAULT_TRANSACTION_ISOLATION;

 default_transaction_isolation

-------------------------------

 read committed

(1 row)

 

postgres=# SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL serializable;

SET

postgres=# show default_transaction_isolation;

 default_transaction_isolation

-------------------------------

 serializable

(1 row)

 

다음은 트랜잭션 격리에 대한 호환성이다.

Isolation Level Dirty Read Nonrepeatable Read Phantom Read Serialization Anomaly
Read uncommitted 허용 가능 가능
가능
Read committed 불가능 가능
가능
가능
Repeatable read 불가능
불가능
허용
가능
Serializable 불가능
불가능
불가능
불가능

Dirty Read는 커밋되지 않은 데이터를 읽을 수 있다.

Nonrepeatable Read는 한 트랜잭션은 다른 트랜잭션에서 커밋한 데이터를 읽을 수 있다. 한 트랜잭션(T1)이 데이터를 Read 하는데, 다른 트랜잭션(T2)이 그 데이터에 접근하여 값을 변경(Update) 또는 삭제(Delete)하고 커밋을 한 후, T1이 다시 그 데이터를 Read 하면 변경된 데이터 혹은 사라진 데이터를 읽을 수 있다.

Phantom Read는 한 트랜잭션은 다른 트랜잭션에서 커밋한 데이터를 읽을 수 있는 상황에서 다른 트랜잭션에 의해 커밋된 자료가 있더라도 항상 자신의 트랜잭션에서 조회했던 그 자료값 그대로 보여줘야 한다. 한 트랜잭션(T1)이 데이터를 Read 하는데, 다른 트랜잭션(T2)이 데이터를 추가(Insert) 또는 삭제(Delete)하고 커밋을 한 후, T1이 다시 그 데이터를 Read 하면 추가된 데이터 혹은 사라진 데이터를 읽을 수 있다.

Serialization Anomaly는 트랜잭션 그룹을 성공적으로 커밋한 결과는 한 번에 하나씩 모든 트랜잭션을 실행할 수 있는 순서와 일치하지 않는다.

Read uncommitted는 커밋하지 않은 데이터를 읽을 수 있다.

Read committed는 질의 시작 전에 커밋된 데이터만 읽을 수 있다

Repeatable read는 트랜잭션 시작 전에 커밋된 데이터만 읽을 수 있다.

Serializable은 트랜잭션을 한 줄로 세워 차례대로 진행되는 것과 같은 결과를 보장한다.

 

  • Read committed 격리 레벨
    • SELECT 질의
      • 질의가 시작했을 때 커밋된 데이터만 접근 가능하고, 트랜잭션 도중에 발생한 커밋되지 않은 수정 사항에는 접근 불가능하다.
      • 질의가 실행중에 커밋되지 않은 데이터 혹은 동시에 실행되고 있던 다른 트랜잭션에서 커밋된 변경에는 접근 불가능하다. 단, 한 트랜잭션에서 SELECT가 여러 번 있고, 각 SELECT 사이에 다른 트랜잭션이 커밋을 했다면 다음에 실행된 SELECT 질의는 변경된 데이터를 읽을 수 있다.
    • UPDATE, DELETE, SELECT FOR UPDATE, SELECT FOR SHARE 질의
      • 행의 검색 측면에서 SELECT 질의와 동일하게 동작한다.
      • 트랜잭션 1에서 데이터를 변경하는 도중 트랜잭션 2가 같은 행에 접근하면 트랜잭션 1이 끝난 후에 접근 가능하다. 이때, 외부 트랜잭션에 의해 변경된 행이 현재 명령의  WHERE 조건에 맞는지 다시 평가한다.

 

  • Repeatable read 격리 레벨 
    • SELECT 질의
      • 트랜잭션이 시작하기 전에 커밋된 데이터만 확인할 수 있다.
      • 외부 트랜잭션에 의해 커밋되거나 커밋되지 않은 변경을 확인할 수 없다. 단, 외부 트랜잭션이 아닌 자신의 트랜잭션 도중에 발생한 커밋되지 않은 수정 사항은 확인할 수 있다.
    • UPDATE, DELETE, SELECT FOR UPDATE, SELECT FOR SHARE 질의
      • 행의 검색 측면에서 SELECT 질의와 동일하게 동작한다.
      • 트랜잭션 1에서 데이터를 변경하는 도중 트랜잭션 2가 같은 행에 접근하면 트랜잭션 1이 끝난 후에 변경하려던 행이 변경됐는지 확인해서 변경사항이 없으면 원래 진행하려던 명령을 실행하고, 변경사항이 있으면 에러메시지(ERROR:could not serialize access due to concurrent update)를 내면서 트랜잭션을 rollback한다.
      • 읽기만 하는 트랜잭션에서는 에러가 없다.
  • Serializable 격리 레벨
    • 가장 엄격한 트랜잭션 격리를 제공한다.
    • 트랜잭션들이 동시에 일어나지 않고, 한 번에 하나씩 순서대로 실행되는 것처럼 작동한다.
    • 잘 고려하여 사용하지 않으면 성능저하가 발생할 수 있다. 

 

 

명시적 잠금(Explicit Locking)

  • 테이블 레벨 잠금

다음은 테이블 레벨 잠금에 대한 호환성이다.

 

테이블 잠금 모드 ACCESS SHARE ROW SHARE ROW EXCLUSIVE SHARE UPDATE EXCLUSIVE SHARE SHARE ROW EXCLUSIVE EXCLUSIVE ACCESS EXCLUSIVE
ACCESS SHARE               X
ROW SHARE             X X
ROW EXCLUSIVE         X X X X
SHARE UPDATE EXCLUSIVE       X X X X X
SHARE     X X   X X X
SHARE ROW EXCLUSIVE     X X X X X X
EXCLUSIVE   X X X X X X X
ACCESS EXCLUSIVE X X X X X X X X

ACCESS SHARE는 SELECT 명령시 획득된다.

ROW SHARE는 SELECT FOR UPDATE 및 SELECT FOR SHARE 명령시 획득된다.

ROW EXCLUSIVE는 UPDATE, DELETE, INSERT 명령시 획득된다.

SHARE UPDATE EXCLUSIVE는 VACUUM, ANALYZE, CREATE INDEX CONCURRENTLY, CREATE STATISTICS, ALTER TABLE VALIDATE 수행시 획득된다. ALTER TABLE의 양식 다수에서 획득된다.

SHARE는 CREATE INDEX 수행시 획득된다.

SHARE ROW EXCLUSIVE는 CREATE COLLATION, CREATE TRIGGER 수행시 획득된다. 

ALTER TABLE의 양식 다수에서 획득된다.

EXCLUSIVE는 REFRESH MATERIALZED VIEW CONCURRENTLY 실행시 획득된다.

ACCESS EXCLUSIVE는 DROP TABLE, TRUNCATE, REINDEX, CLUSTER, VACUUM FULL, REFRESH MATERIALIZED VIEW 명령시 획득된다. ALTER TABLE의 양식 다수에서 획득된다.

테스트:https://www.postgresql.org/docs/current/static/sql-lock.html

  • 행 레벨 잠금

다음은 테이블 레벨 잠금에 대한 호환성이다.

 

행 잠금 모드 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

FOR UPDATE는 SELECT, UPDATE, DELETE에 의해 획득된다. 이는 현재 트랜잭션이 끝나기 전까지 다른 트랜잭션이 이 행에 대해 UPDATE, DELETE, SELECT FOR UPDATE, SELECT FOR NO KEY UPDATE, SELECT FOR SHARE, SELECT FOR KEY SHARE를 시도하는 것이 차단된다.

FOR NO KEY UPDATE는 FOR UPDATE보다 잠금이 약하다는 점 외에는 비슷하다. 이는 현재 트랜잭션이 끝나기 전까지 다른 트랜잭션이 이 행에 대해 UPDATE, DELETE, SELECT FOR UPDATE, SELECT FOR NO KEY UPDATE, SELECT FOR SHARE를 시도하는 것이 차단된다.

FOR SHARE는 FOR NO KEY UPDATE보다 검색된 각 행에 독점 잠금이 아닌 공유 잠금을 획득한다는 점 외에는 동일하다. 이는 현재 트랜잭션이 끝나기 전까지 다른 트랜잭션이 이 행에 대해 UPDATE, DELETE, SELECT FOR UPDATE, SELECT FOR NO KEY UPDATE를 시도하는 것이 차단된다.

FOR KEY SHARE는 FOR SHARE보다 잠금이 약하다는 점 외에는 비슷하다. 이는 현재 트랜잭션이 끝나기 전까지 다른 트랜잭션이 이 행에 대해 UPDATE, DELETE, SELECT FOR UPDATE를 시도하는 것이 차단된다.

 

  • 페이지 레벨 잠금

공유 버퍼 풀에 있는 테이블 페이지에 Read/Write 접근을 제어하는데 사용된다.

애플리케이션 개발자는 페이지 레벨 잠금을 신경쓸 필요가 없다. 행이 페치 또는 갱신된 후 즉시 이 잠금이 해제되기 때문이다.

 

 

데드락(Deadlock)

명시적 잠금은 2개 이상의 트랜잭션을 수행시 데드락 가능성이 증가될 수 있다.

예를 들어 트랜잭션 1은 독점 잠금으로 테이블 A를 획득한 다음에 테이블 B를 독점 잠금으로 획득하려고 시도하고, 트랜잭션 2는 독점 잠금으로 테이블 B를 획득한 다음에 테이블 A를 독점 잠금으로 획득하려고 할 때, 어느 트랜잭션도 진행되지 않는 상태에 빠지게 된다.

이러한 상황을 데드락이라고 말하며, PostgreSQL은 데드락 상황을 자동으로 감지하고 관련 트랜잭션 중 하나를 중단하여 나머지가 완료되도록 해서 해결한다. 정확히 어떤 트랜잭션이 중단될지 예측하기에는 어렵다.

 

경고성 잠금(Advisory lock)

애플리케이션에서 잠금을 생성할 수 있는 수단을 제공한다. 시스템은 이 잠금의 사용을 강제하지 않으며 애플이케이션이 잠금을 바르게 사용하는 것에 달려 있기 때문에 이를 Advisory lock이라고 한다.

경고성 잠금은 MVCC 모델을 적용하기 어색한 전략에 유용하다. 

경고성 잠금은 세션 레벨과 트랜잭션 레벨 두가지 방법으로 획득한다.

세션 레벨에서 경고성 잠금을 획득한 경우 명시적으로 해제하거나 세션이 끝날 때까지 유지된다. 

트랜잭션 레벨에서 경고성 잠금을 획득한 경우 명시적으로 해제할 수 없고 트랜잭션 종료시 자동으로 해제된다.

예를 들어 애플리케이션에서 공유 문서에 대한 접근을 조작하는 경우이며, 조작하기 위해 PosgreSQL에서 제공되는 함수를 사용할 수 있다.

SELECT pg_advisory_lock(id) FROM foo WHERE id = 1;


경고성 잠금을 조작하기 위해 제공되는 함수는 다음과 같다.

함수명반환 타입설명경고성 잠금 레벨

pg_advisory_lock(key bigint) void Obtain exclusive session level advisory lock Session-level
pg_advisory_lock(key1 int, key2 int) void Obtain exclusive session level advisory lock Session-level
pg_advisory_lock_shared(key bigint) void Obtain shared session level advisory lock Session-level
pg_advisory_lock_shared(key1 int, key2 int) void Obtain shared session level advisory lock Session-level
pg_advisory_unlock(key bigint) boolean Release an exclusive session level advisory lock Session-level
pg_advisory_unlock(key1 int, key2 int) boolean Release an exclusive session level advisory lock Session-level
pg_advisory_unlock_all() void Release all session level advisory locks held by the current session Session-level
pg_advisory_unlock_shared(key bigint) boolean Release a shared session level advisory lock Session-level
pg_advisory_unlock_shared(key1 int, key2 int) boolean Release a shared session level advisory lock Session-level
pg_advisory_xact_lock(key bigint) void Obtain exclusive transaction level advisory lock Transaction-level
pg_advisory_xact_lock(key1 int, key2 int) void Obtain exclusive transaction level advisory lock Transaction-level
pg_advisory_xact_lock_shared(key bigint) void Obtain shared transaction level advisory lock Transaction-level
pg_advisory_xact_lock_shared(key1 int, key2 int) void Obtain shared transaction level advisory lock Transaction-level
pg_try_advisory_lock(key bigint) boolean Obtain exclusive session level advisory lock if available Session-level
pg_try_advisory_lock(key1 int, key2 int) boolean Obtain exclusive session level advisory lock if available Session-level
pg_try_advisory_lock_shared(key bigint) boolean Obtain shared session level advisory lock if available Session-level
pg_try_advisory_lock_shared(key1 int, key2 int) boolean Obtain shared session level advisory lock if available Session-level
pg_try_advisory_xact_lock(key bigint) boolean Obtain exclusive transaction level advisory lock if available Transaction-level
pg_try_advisory_xact_lock(key1 int, key2 int) boolean Obtain exclusive transaction level advisory lock if available Transaction-level
pg_try_advisory_xact_lock_shared(key bigint) boolean Obtain shared transaction level advisory lock if available Transaction-level
pg_try_advisory_xact_lock_shared(key1 int, key2 int) boolean Obtain shared transaction level advisory lock if available Transaction-level

 

애플리케이션 레벨에서 데이터 일관성 검사

  • 직렬화 트랜잭션을 이용한 일관성 강제
    • Serializable 사용할 수 있는 경우에 사용한다. 
    • 데이터의 일관된 뷰가 필요한 모든 쓰기 및 읽기에 사용하여 일관성을 강제한다.
  • 명시적 차단 잠금을 사용한 동시성 강제
    • Serializable 사용할 수 없는 경우에 사용한다.
    • 예를 들어 테이블 잠금 모드인 SHARE는 현재 트랜잭션 외에 잠긴 테이블에서 커밋되지 않은 변경이 없다는 것을 보장한다.

 

인덱스(Index)

비차단 읽기/쓰기 접근은 현재 PostgreSQL에서 구현된 모든 인덱스 액세스 방법에 대해 제공되지 않는다.

  • B-tree, GiST, SP-GiST
    • 페이지 레벨 잠금이 사용된다.
    • 인덱스 행이 페치 또는 삽입된 후 잠금이 즉시 해제된다.
    • 데드락 상태 없이 최고의 동시성을 제공한다.
  • Hash
    • 해시 버킷 레벨 잠금이 사용된다.
    • 전체 버킷이 처리된 후 잠금이 해제된다.
    • 데드락 상태가 발생할 수 있지만 뛰어난 동시성을 제공한다.
  • GIN
    • 페이지 레벨 잠금이 사용된다.
    • 인덱스 행이 페치 또는 삽입된 후 잠금이 즉시 해제된다.
    • 일반적으로 행별로 몇개의 인덱스 키 삽입이 생성되므로 GIN은 단일 값 삽입에 대한 중요한 작업을 수행할 수 있다.

스칼라 데이터를 인덱싱할 때에는 B-tree를 사용해야 한다.

비스칼라 데이터를 인덱싱할 때에는 B-tree는 유용하지 않으며 GiST, SP-GiST, GIN을 사용해야 한다.

 

참고:

How do PostgreSQL advisory locks work: https://vladmihalcea.com/how-do-postgresql-advisory-locks-work/