[번역글] PostgreSQL에서의 트랜잭션 격리 수준
트랜잭션의 고립성에 대한 오해는 어플리케이션에서 아주 심각한 사이드 이펙트를 겪을 수 있다. 이런 이슈들을 디버깅하는 과정은 매우 고통스럽기까지 하다. 표준 SQL은 네가지 트랜잭션 격리성을 정의하였다. 각각의 격리수준은 만약 두개의 동시 프로세스가 동작하고 있을 때, 어떤 일이 일어날지에 대해서 정의한다.
해당 게시글은 어떻게 PostgreSQL이 기본적으로 어떤 격리수준을 가지는지 탐구하고 대체적으로 선택할 수 있는 데이터 정합성 기반으로 확신할 수 있는 옵션은 어떤것이 있을지 설명한다. 또한 다양한 격리 수준에 대한 성능 수준에 대해서도 탐구해볼 예정이며 각각 사용 케이스들을 보고자 한다.
동시 트랜잭션의 격리
이론적인 설명에 들어가기 전에, PostgreSQL의 기본적인 수행을 살펴보자. 일단 동시 프로세스 두개가 같은 값에 동시에 접근할때 어떤 일이 일어나는지 알고 싶다. 두 트랜잭션은 완전히 서로 격리되어 있을까?
process A: begin; process A: SELECT sum(value) FROM purchases; --- process A sees that the sum is 1600 process B: INSERT INTO purchases (value) VALUES (400) --- process B inserts a new row into the table while --- process A's transaction is in progress process A: SELECT sum(value) FROM purchases; --- process A sees that the sum is 2000 process A: COMMIT; |
기본적으로 SQL에서의 트랜잭션은 Read Committed 격리 수준으로 되어있다. 성공한 두개의 select 결과는 다른 값을 리턴한다. 위의 예에서 A 프로세스는 1600을 리턴하고 B프로세스가 값을 변화시킨다.(삽입을 하므로) 그로인해 2000이라는 값이 계산된다.
대부분의 개발자들은 단일 트랜잭션에서는 두 select 쿼리가 정확히 같을 거라고 예상할 것이다. 하지만 이것은 완전히 잘못되었다. 개발자들이 트랜잭션이 수행되는 동안 이것을 고려하지 못한다면 엄청난 버그에 이를 것이다.
표준 SQL에서의 4가지 격리 수준
표준 SQL은 트랜잭션의 네가지 격리 수준을 정의한다. 가장 강력한 고립 수준은 Serializable 수준이다. 다른 격리수준은 트랜잭션이 동시적으로 실행될 때 발생할 수 있는 허용 수준의 관점으로 정의되었다.
- Serializable 격리 수준은 동시 트랜잭션이 마치 직렬적으로 하나씩 처리되는 결과를 보장할 수 있다.
- 한단계 낮은 수준의 Read Repeatable 격리수준은 Phantom Read(삽입 이상)를 허용한다. Serializable 격리수준에서의 실행 결과와는 대조적으로 연이은 select 쿼리의 결과로 나온 값이 다를 수 있다. 이는 같은 select 쿼리를 수행할 때 값이 추가 혹은 삭제될 경우 발생할 수 있다.
- 한단계 더 낮은 수준의 격리 수준은 Read Commited이다. 한 트랜잭션 내에서 두개의 연속된 select 문은 다른 값을 가질 수 있다. Read Repeatable 의 결과와는 달리 해당 수준에서는 레코드의 수와 같은 로우셋 뿐만아니라 레코드의 데이터 마저도 다르게 조회될 수 있다.(한 트랜잭션 내에서라도) 다른 트랜잭션에 의해 로우의 값이 수정된 경우에 해당한다.
- 가장 낮은 수준의 격리 수준은 Read Uncommited이다. Dirty read 라는 이상이 발생할 수 있다. 다른 트랜잭션의 커밋되지 않은 데이터마저 손을 댈 수 있는 수준이다.
Read Uncommited는 PostgreSQL에서는 제공되지 않는 수준이다. 만약 해당 격리 수준을 사용하고자 한다면, 기본 형태인 Read Commited로 수행될 것이다.
Read Commited와 Read Repeatable 격리 수준의 비교
실제로 예제를 보면서 비교를 해보도록 하겠다. 예제를 통해 이러한 격리수준의 차이점을 이해하는데 도움이 될 것이다. 우선 트랜잭션 격리수준을 탐구하고 부작용을 찾아보도록 하겠습니다.
먼저 디폴트인 Read Committed 격리수준을 사용하여 첫번째 섹션의 예제를 보겠습니다.
process A: BEGIN; -- the default is READ COMMITED process A: SELECT sum(value) FROM purchases; --- process A sees that the sum is 1600 process B: INSERT INTO purchases (value) VALUES (400) --- process B inserts a new row into the table while --- process A's transaction is in progress process A: SELECT sum(value) FROM purchases; --- process A sees that the sum is 2000 process A: COMMIT; |
트랜잭션 수명동안 프로세스 A에서 합계 값이 변경되는 것을 방지하려면 Repeatable Read를 사용할 수 있습니다.
process A: BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ; process A: SELECT sum(value) FROM purchases; --- process A sees that the sum is 1600 process B: INSERT INTO purchases (value) VALUES (400) --- process B inserts a new row into the table while --- process A's transaction is in progress process A: SELECT sum(value) FROM purchases; --- process A still sees that the sum is 1600 process A: COMMIT; |
A프로세스의 트랜잭션은 스냅샷을 찍어놓고 일관성 있는 값을 트랜잭션의 생명주기동안 제공할 것 입니다. Repeatable Read는 디폴트인 Read Committed 보다 더 비싸거나 하지 않습니다. 그러므로 성능적 페널티를 걱정할 필요가 없습니다. 그러나 어플리케이션은 직렬화 실패로 인한 트랜잭션의 재시도를 준비해야 합니다.
Repeatable read 격리수준을 활용할때 나타날 수 있는 이슈에 대해서 살펴보겠습니다. (could not serialize access due to concurrent update) 에러
process A: BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ; process B: BEGIN; process B: UPDATE purchases SET value = 500 WHERE id = 1; process A: UPDATE purchases SET value = 600 WHERE id = 1; -- process A wants to update the value while process B is changing it -- process A is blocked until process B commits process B: COMMIT; process A: ERROR: could not serialize access due to concurrent update -- 프로세스 A는 프로세스 B를 커밋하고자 할때 에러가 난다. |
프로세스 B가 롤백하면 변경사항이 무효화되고 문제없이 repeatable read를 진행할 수 있습니다. 그러나 프로세스 B가 변경사항을 커밋하면 repeatable read 트랜잭션이 시작된 후 다른 프로세스에 의해 변경된 행을 수정하거나 잠글 수 없기 때문에 repeatable read 트랜잭션은 오류메시지와 함께 롤백됩니다.
Read Repeatable vs Serializable 격리 수준 비교
Serializable 격리 수준은 가장 강력한 격리를 제공합니다. 아이디어는 간단합니다. 한 트랜잭션이 하나의 프로세스에서 정확히 동작하는 것으로 인지된다면 다른 많은 프로세스에서 동작할 때도 정확하게 동작할 것이다.
이런 보장에는 대가가 따릅니다. Serializable 트랜잭션은 Serializable 오류가 자주 발생하고 추가적인 성능 비용이 발생할 수 있습니다. PostgreSQL 엔진에 대한 깊은 이해가 있는 경우에만 Serializable 트랜잭션을 사용하는 것이 좋을 것 같습니다.
SQL 표준은 Phantom Read를 허용합니다. Phantom Read란 동시 프로세스에 의해 현재 프로세스의 select 결과의 로우 수에 영향을 미치게 하는 버그입니다. 그러나 PostgreSQL에서는 Repeatable Read에서 Phantom Read로 부터 보호하고 있습니다.
PostgreSQL에서 Serializable 과 Repeatable Read의 차이점이 무엇인지 궁금할 것입니다. 두 격리 모드 간의 차이점을 보여주는 두 가지 예를 비교해 보겠습니다.
process A: BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ; process A: SELECT sum(value) FROM purchases; process A: INSERT INTO purchases (value) VALUES (100); process B: BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ; process B: SELECT sum(value) FROM purchases; process B: INSERT INTO purchases (id, value); process B: COMMIT; process A: COMMIT; |
Repeatable Read로 설정한다면 정상적으로 동작하겠지만 Serializable 모드로 사용한다면 프로세스 A에서 오류가 발생합니다.
process A: BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; process A: SELECT sum(value) FROM purchases; process A: INSERT INTO purchases (value) VALUES (100); process B: BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; process B: SELECT sum(value) FROM purchases; process B: INSERT INTO purchases (id, value); process B: COMMIT; process A: COMMIT; ERROR: could not serialize access due to read/write dependencies among transactions DETAIL: Reason code: Canceled on identification as a pivot, during commit attempt. HINT: The transaction might succeed if retried. |
두 트랜잭션 모두 다른 트랜잭션에서 읽은 내용을 수정하려고 합니다. 둘다 커밋을 허용한다면 직렬화 가능 동작을 위반할 것입니다. 한번에 하나씩 실행되는 경우 트랜잭션중 하나가 다른 트랜잭션에 의해 삽입된 새 레코드를 보았을 것이기 때문입니다.
원본 참조 : http://morningcoffee.io/transaction-isolation-levels-in-postgresql.html