[PSQL] Transaction Isolation 살펴보기

dojinyou·2023년 12월 24일
0

Transaction Isolation

Important

몇몇 데이터 타입과 함수는 트랜잭션 동작과 관련된 특별한 규칙이 있습니다.

  • Serial Types
    • smallserial, serial, bigserial 3가지 타입이 있고 이는 고유한 식별자를 만들기 위한 표현일뿐 실제 타입이 아닙니다.
    • 아래의 SQL을 보면 사용과 실제 동작의 차이를 알 수 있습니다.
      // 사용할 때
      CREATE TABLE tablename (
          colname SERIAL
      );
      
      // 동작
      CREATE SEQUENCE tablename_colname_seq AS integer;
      CREATE TABLE tablename (
          colname integer NOT NULL DEFAULT nextval('tablename_colname_seq')
      );
      ALTER SEQUENCE tablename_colname_seq OWNED BY tablename.colname;
      SEQUCE를 생성하고 이를 특정 필드에 소유시켜 기본값으로 설정하는 형식입니다. 기본 값이기 때문에 다른 값을 임의로 삽입할 수 있고 소유하는 컬럼이 삭제될 경우 함께 삭제됩니다.
    • 별도의 Unique 혹은 Primary Key 제약 조건을 주지 않는다면 중복을 허용합니다.
    • Sequncenextval 함수는 원자적으로 수행됩니다. 따라서 nextval 호출을 포함하는 트랜잭션이 롤백되더라도 sequance 값을 회수하지 않습니다.

Read Commited Isolation Level

  • PSQL의 기본 격리 수준입니다.

  • SELECT문은 쿼리 시점 이전에 commit 된 데이터 혹은 같은 트랜잭션 내의 변경 사항만을 볼 수 있습니다. 2개의 질의 중간에 commit 된 데이터가 존재한다면 나중 질의에서는 조회가 될 수 있습니다.

  • UPDATE, DELETE, SELECT FOR UPDATE(or FOR SHARE)에서도 모두 질의는 동일하게 동작합니다.

  • 아래 그림은 Read Commited Isolation Level에서의 동작을 표현하고 있습니다.
    1

    • Client A의 Insert로 인해 id=2인 데이터가 생성되어도 Client B에게는 노출되지 않습니다.
    • Client B의 트랜잭션 내 Insert된 id=3 인 데이터는 조회가 됩니다.
    • Client B의 트랜잭션이 Commit 되고 난 뒤에 Client A는 id=3 인 데이터를 조회할 수 있습니다.

Repeatable Read Isolation Level

  • 반복 읽기 격리 수준은 트랜잭션이 시작되기 전에 커밋된 데이터만 볼 수 있습니다. 트랜잭션이 진행되는 동안 커밋 되거나 아직 커밋 되지 않는 내용을 볼 수 없습니다. 같은 트랜잭션 내의 변경 사항을 볼 수 있습니다.

  • 아래 그림은 Repeatable Read Isolation Level에서의 단순 읽기 동작을 표현하고 있습니다.

    • Client A의 마지막 Select 에서도 Client BCommit 된 데이터(id=3)가 조회되지 않습니다.(phantom read 발생 x) 이전 Select 결과에 내부 트랜잭션에서 Insert된 데이터(id=2) 만 추가되어 조회 됩니다.
  • 아래 그림은 Repeatable Read Isolation Level에서의 같은 Row에 대한 동시 업데이트를 표현하고 있습니다.

    • Client BUpdateClient AUpdate같은 row를 변경하려고 합니다. 이때 Client A의 lock에 의해서 대기하게 됩니다.
    • Client A의 Transaction이 Commit 될 경우, Client B의 Transaction은 에러([40001] ERROR: could not serialize access due to concurrent update)와 함께 Rollback 됩니다.
    • Client A의 Transaction이 Rollback 될 경우, Client B의 Transaction은 정상적으로 수행됩니다.
  • 반복 읽기 수준은 위와 같이 트랜잭션이 안정적으로 db를 제어하도록 보장하지만 항상 연속된 실행(serial)의 결과와 같은 결과를 보장하지는 않습니다. Write Skew 현상으로 트랜잭션 내부에서 문제 없이 의사결정되지만 트랜잭션의 결과가 결과적으로 논리적 제약사항을 위반하게 되는 것을 의미합니다. 대표적으로 의사 당직 문제가 있습니다.

    • 의사 당직 문제: 의사는 최소 1명 이상 당직을 서야 합니다. 따라서 자신 외에 당직 의사가 1명 이상(자신을 포함해서 2명 이상) 있는 것은 확인하고 당직을 종료하게 됩니다.

      UPDATE doctor
      SET on_call = false
      WHERE (2 <= SELECT count(id) FROM doctokrs WHERE on_call = true) 
      AND id = '1'
      • 위 업데이트문을 보면 두 클라이언트의 트랜잭션 내에서 조건을 만족하고 정상 수행되는 것이 기대가 됩니다. 하지만 실제로 트랜잭션이 종료되고 난 후에 결과는 당직 의사가 없어지게 되는 문제를 맞이하게 됩니다.
      • 이를 직렬화와 같이 순차적으로 수행하게 된다면 늦게 실행된 트랜잭션의 업데이트문은 해당 row가 없기 때문에 실행되지 않고 1명의 의사가 온전히 남아 있게 될 것입니다.

Serializable Read Isolation Level

  • 직렬화 가능한 격리 수준은 가장 엄격한 격리 수준으로 마치 순차적으로 실행되는 것과 같은 결과를 보장해야 합니다. 앞서 의사 당직 문제가 생기지 않아야 한다는 것입니다.
  • 실제로 PSQL의 직렬화 가능한 격리수준은 반복 읽기와 동일하게 동작합니다. 하지만 직렬화 가능하지 않은 상태를 감지하고 이러한 감지에 따라 트랜잭션이 실패 된다는 차이가 있습니다. 따라서 실패 시에 재시도에 대한 전략 역시 준비되어야 합니다.
  • 직렬화 가능한 격리수준에서 의사 문제는 다음과 같은 결과를 보여줍니다.
    • Client B의 Transaction COMMIT 시에 ERROR가 발생합니다. 즉, psql에 직렬화 되지 않는 상황을 감지하고 트랜잭션을 취소 시킵니다.
  • 트랜잭션 내에서 읽은 정보를 의존해서는 안됩니다.
  • PSQL은 직렬화를 보장하기 위해 predicate locking을 사용합니다. 이는 쓰기 동작이 동시에 진행되는 트랜잭션에서 발생한 이전의 읽기에 영향을 줄 수 있는지 판단하기 위한 잠금을 유지한다는 것입니다. 실제로 어떠한 교착 상태를 발생시키지는 않습니다. 다만 특정 조합에서 직렬화가 되지 않을 수 있는 트랜잭션과의 종속성을 식별하는데 사용됩니다.
  • predicate locking은 트랜잭션이 실제로 접근한 데이터를 기반으로 하며, pg_locks 테이블에 SIReadLock 모드으로 보여집니다.
  • 직렬화 가능한 트랜잭션을 일관되게 사용하면 편리하게 정합성을 보장하며 개발할 수 있습니다. 다만, 어떤 트랜잭션이 읽기/쓰기 종속성에 기여할 수 있고 직렬화 이상을 방지하기 위해 롤백해야하는지 정확히 예측하기 어렵기 때문에 항상 SQLSTATE 40001 에러에 대한 일반적인 처리 방법이 필요합니다.
  • 읽기/쓰기 종속성 모니터링과 실패한 트랜잭션의 재시도라는 비용이 추가적으로 발생하지만 명시적인 락을 통한 차단을 이용할 때보다 더 나은 성능을 보일 수 있습니다.
  • PSQL의 직렬화 가능한 트랜잭션 격리 수준이 실제로 순서대로 실행

PSQL Serializable Snapshot Isolation은 어떻게 이상 징후를 감지할까?

  • 이론적 배경
    • 동시에 발생되는 트랜잭션들의 종속성은 크게 3가지로 구성됩니다.
      1. wr-dependencies: T1이 객체의 버전을 작성하고 T2가 해당 버전을 읽으면 T1이 T2보다 먼저 실행된 것으로 보입니다. 실제로 T2에 T1의 변경사항이 보이기 위해서는 T2가 실행되기 전에 T1이 커밋을 해야합니다.
      2. ww-dependencies: T1이 어떤 객체의 버전을 작성하고 T2가 그 버전을 다음 버전으로 대체하면 T1이 T2보다 먼저 실행된 것으로 보입니다. 해당 의존성 역시 쓰기 잠금으로 인해 T1이 커밋된 이후에 T2 트랜잭션이 실행되어야 합니다.
      3. rw-antidependencies: T1이 어떤 객체의 버전을 작성하고 T2가 그 이전 버전을 읽었다면 T1의 변경사항을 보지 못했기 때문에 T2는 T1보다 먼저 실행된 것으로 보입니다. 이러한 종속성은 동시에 진행되는 트랜잭션에서 발생되며 SI의 이상현상의 핵심으로 이를 rw충돌(rw-conflicts)이라 합니다.
    • 동시에 진행되는 트랜잭션들 사이에 rw충돌이 발생할 수 있습니다. 특히, 한 트랜잭션은 rw충돌의 대상이면서 시작점일 수 있습니다. 이러한 경우를 사이클이 생길 수 있는 위험한 구조(dangerous structure)로 보고 이를 감지합니다. 예를 들어, T1 rw→ T2 rw→ T3 와 같은 의존관계를 가진다면 이를 위험한 구조라고 부를 수 있습니다.
    • 위험한 구조에서 T1이 T2나 T3보다 먼저 커밋이 된다면 스냅샷 격리의 이상현상을 회피할 수 있습니다. 따라서 커밋되는 시점을 통해 불필요한 중단을 줄입니다.
  • PSQL의 구현
    • 읽기 시에는 SIREAD lock을 획득합니다. 실제로 잠금정보를 저장할 뿐 차단은 하지 않습니다. 쓰기 시에 충돌하는 SIREAD가 있는 지 확인합니다.
    • 각 트랜잭션마다 들어오고 나가는 rw충돌에 대해 기억합니다.
    • 일부 트랜잭션은 커밋된 이후에도 rw충돌에 대한 정보가 필요하여 기록하고 있으며 일정 개수가 넘어가면 요약하여 저장합니다.
    • 이를 통해 rw충돌이 들어오고 나가는 위험한 트랜잭션의 커밋조건이 달성될 경우 위험한 구조의 트랜잭션을 중단합니다.

reference

https://www.postgresql.org/docs/15/transaction-iso.html

https://www.postgresql.org/docs/15/functions-sequence.html

https://www.postgresql.org/docs/15/datatype-numeric.html#DATATYPE-SERIAL

https://www.postgresql.org/docs/15/biblio.html#BERENSON95

https://www.postgresql.org/docs/15/biblio.html#PORTS12

profile
더 좋은 세상을 만드는 데 기술로 기여하고 싶습니다

0개의 댓글