Concurrency Control은 ACID에 해당하는 Atomicity, Isolation 을 관리하는 메커니즘
Concurrency Control를 구현하는 방식은 크게 다음과 같다
Oracle의 경우, 롤백 세그먼트를 통해 SI를 구현;
기존 버전 아이템이 롤백 세그먼트에 write 후, 신규 버전 아이템이 data area에 write
PostgreSQL의 경우, visibility check rules를 적용하는 것을 통해 SI를 구현;
신규 데이터는 그냥 직접적으로 관련 테이블의 페이지에 저장된다
데이터 아이템 조회 시, 각각 트랜잭션에 대해 적합한 버전의 아이템을 조회
PostgreSQL이 구현한 SI는 ANSI SQL-92 표준 3가지 이상 동작 (Dirty Reads, Non-Repeatable Reads, Phantom Reads)를 허용하지 않으나, serialization 이상동작(Write Skew, Read-Only Transaction Skew)을 허용하기 때문에 진정한 의미의 Serializability를 실현하진 않는다
(즉, Serializable 레벨에서 Serialization Anomaly 가 일어날 수도 있다)
이런 이슈를 대처하기 위해, PostgreSQL 9.1 버전부터 Serializable Snapshot Isolation(SSI)가 추가되었다
Serializable Snapshot Isolation(SSI)
Transaction wraparound problem?
image reference
(2^31 + 101 txid 입장에서 보았을때, 100 txid가 미래 txid라고 인식되어서 이미 100 txid를 통해 저장한 데이터가 invisible한 상황에 대한 예시)
loop되는 txid 할당 구조에서, 과거 txid에서 저장한 데이터가 현재 할당된 txid 기준에서 미래 txid라고 인식되어 visible하지 않게 되는 현상
현재 txid
- vacuum_freeze_min_age
)보다 오래된 txid를 frozen id(2)로 바꾸는 프로세스 테이블 정보를 담는 Heap 파일 및 페이지에 저장되는 Tuple은 크게 2가지로 나뉜다
Data Tuple
HeapTypleHeaderData 구조체, NULL bitmap, user data로 구성된 페이지에 저장되는 튜플
TOAST Tuple
TOAST?
새로운 튜플을 삽입하면 신규 튜플의 각 튜플 헤더 데이터는 다음과 같이 설정된다
기존 튜플을 삭제하면 기존 튜플의 튜플 헤더 데이터는 다음과 같이 설정된다
삭제를 진행하는 트랜잭션이 commit 된 후, 필요없게 된 해당 튜플은 Dead Tuple로 간주된다
Auto Vacuum에 의해서 Dead Tuple은 주기적으로 정리된다
업데이트는 최신 버전의 튜플 데이터가 페이지에 추가되고, 기존 버전의 튜플이 논리적 제거(데드 튜플로써 추후 Vacuum 대상)가 이루어진다
image reference
txid=99 트랜잭션에서 삽입된 어떤 레코드가 txid=100 트랜잭션을 거치면서 업데이트 되는 과정 예시;
txid=100 트랜잭션 내부에는 업데이트 쿼리가 총 2개 있다
이 예시의 트랜잭션에 따라 추가된 힙 튜플들이 저장된 페이지의 번호는 0번이며, Line Pointer는 1번부터 시작하는 것을 가정한다
txid=100 트랜잭션 commit 후,
Tuple_1과 Tuple_2는 논리적으로 제거된 상태(Dead Tuple)가 되어, Vacuum 대상이 된다
pg_xact
서브 디렉토리에 저장됨트랜잭션이 시작하면 IN_PROGRESS 상태가 되고, 트랜잭션이 COMMIT 또는 ABORT 명령을 수행하면 COMMITTED, ABORTED로 상태값이 업데이트 된다
각 트랜잭션의 특정 시점에 활성화 중인 모든 트랜잭션들의 상태에 대한 정보를 담고 있는 것
PostgreSQL이 MVCC를 구현하기 위해 사용하는 Visibility Check를 위해 활용
xmin:xmax:xip_list 형태로 표현되며 각 txid는 다음과 같은 뜻을 내포
image reference (snapshot 표현식 예시)
스냅샷은 격리 레벨에 따라 제공되는 주기가 다르다
스냅샷으로 visibility check를 수행할 때, 스냅샷 내부에서 활성 중으로 인식된 트랜잭션은 실제로 이미 committed나 aborted 되었더라도, 아직 스냅샷과 동일하게 진행중(활성중)으로 인지되어야 한다
READ COMMITTED, REPEATABLE READ, SERIALIZABLE 모든 격리 레벨에서 visibility check가 다르게 동작하는 것을 고려해야 하기 때문
image reference
(각기 다른 격리 레벨을 가진 트랜잭션들의 스냅샷 할당 상태에 대한 예시)
if t_xmin == IN_PROGRESS &&
t_xmin == current_txid && t_xmax == INVALID --> Visible
현재 작업 진행중인 트랜잭션에서 최초로 생성된 튜플이면 해당 트랜잭션 내부에서는 Visible
t_xmin == current_txid && t_xmax != INVALID --> Invisible
현재 작업 진행중인 트랜잭션에서 최초로 생성된 튜플인데 추가 수정이나 삭제가 되었다면, Invisible
t_xmin != current_txid --> Invisible
현재 작업 진행중인 트랜잭션에서 생성된 튜플은 해당 트랜잭션 내부가 아니면, Invisible
if t_xmin == COMMITTED &&
t_xmin is active in snapshot --> Invisible
커밋 완료 튜플이지만, 스냅샷 내부에 아직 튜플 생성 트랜잭션이 활성화로 있다면 Invisible
(t_xmax == INVALID || t_xmax == ABORTED) --> Visible
커밋 완료 튜플이 변경 및 삭제된 내역이 없다면 Visible
t_xmax == IN_PROGRESS && t_xmax == current_txid --> Invisible
커밋 완료 튜플을 변경 및 삭제중인 트랜잭션 내부는 Invisible
t_xmax == IN_PROGRESS && t_xmax != current_txid --> Visible
커밋 완료 튜플을 변경 및 삭제중인 트랜잭션 외부는 Visible
t_xmax == COMMITTED && t_xmax is active in snapshot --> Visible
커밋 완료 튜플의 갱신을 커밋했지만, 스냅샷 내부에 갱신 트랜잭션이 활성화로 있다면 Visible
t_xmax == COMMITTED && t_xmax is inactive --> Invisible
커밋 완료 튜플의 갱신을 커밋했고, 스냅샷 내부에 해당 갱신 트랜잭션이 없다면 Invisible
PostgreSQL은 위에 언급한 Visibility Check Rules를 이용하여, 특정 버전의 튜플을 조회 시 Visible 한지, Invisible 한지 체크하여 각 트랜잭션마다 각기 버전이 다른 튜플을 보여줌으로써 MVCC를 구현할 수 있다
image reference
위 예시에서 각 시간대 별로 다음과 같은 Rule을 적용하여 Visiblity를 판단한다
T3
Visibility(Tuple_1) Rule6
= t_xmin == COMMITTED && t_xmax == INVALID
= Visible
T5
txid = 200
Visibility(Tuple_1) Rule7
= t_xmin == COMMITTED && t_xmax == IN_PROGRESS && t_xmax == current_txid
= Invisibile
Visibility(Tuple_2) Rule2
= t_xmin == IN_PROGRESS && t_xmin == current_txid && t_xmax == INVALID
= Visible
txid = 201
Visibility(Tuple_1) Rule8
= t_xmin == COMMITTED && t_xmax == IN_PROGRESS && t_xmax != current_txid
= Visibile
Visibility(Tuple_2) Rule4
= t_xmin == IN_PROGRESS && t_xmin != current_txid
= Invisible
T7
isolation level == READ_COMMITTED
Visibility(Tuple_1) Rule10
= t_xmin == COMMITTED && t_xmax == COMMITTED && snapshot(t_xmax) != active
= Invisible
Visibility(Tuple_2) Rule6
= t_xmin == COMMITTED && t_xmax == INVALID
= Visible
isolation level == REPEATABLE_READ
Visibility(Tuple_1) Rule9
= t_xmin == COMMITTED && t_xmax == COMMITTED && snapshot(t_xmax) == active
= Visibile
Visibility(Tuple_2) Rule5
= t_xmin == COMMITTED && snapshot(t_xmax) == active
= Invisible
Lost Update(같은 로우를 서로 다른 트랜잭션이 동시에 갱신하려고 할 때 발생하는 이상 동작) 를 PostgreSQL은 Update 쿼리 실행 시 수행하는 함수 내부 로직을 통해 방지한다
UPDATE 쿼리 내부 ExecUpdate 함수의 수도코드
reference
(1) FOR each row that will be updated by this UPDATE command
(2) WHILE true
/*
* The First Block
*/
(3) IF the target row is 'being updated' THEN
(4) WAIT for the termination of the transaction that updated the target row
(5) IF (the status of the terminated transaction is COMMITTED)
AND (the isolation level of this transaction is REPEATABLE READ or SERIALIZABLE) THEN
(6) ABORT this transaction /* First-Updater-Win */
ELSE
(7) GOTO step (2)
END IF
/*
* The Second Block
*/
(8) ELSE IF the target row has been updated by another concurrent transaction THEN
(9) IF (the isolation level of this transaction is READ COMMITTED THEN
(10) UPDATE the target row
ELSE
(11) ABORT this transaction /* First-Updater-Win */
END IF
/*
* The Third Block
*/
ELSE /* The target row is not yet modified */
/* or has been updated by a terminated transaction. */
(12) UPDATE the target row
END IF
END WHILE
END FOR
postgreSQL 객체(tuple, pk 등..)와 현재 해당 객체를 참조하려는 트랜잭션들을 쌍으로 구성하는 Lock 객체
e.g.,
{Tuple_1, {100}}: Tuple_1 객체에 현재 txid=100인 트랜잭션이 접근하려고 한다
{Tuple_2, {100,101}}: Tuple_2 객체에 현재 100,101 트랜잭션이 접근하려고 한다
SIREAD lock 내부에 트랜잭션들 간에 precedence Graph 작성 후, rw-conflict 현상 발생 시 rw-conflict 객체를 생성 한다