Isolation Levels & MVCC(Multiversion concurrency control)

Wonjun Lee·2025년 11월 27일

Isolation level

격리 레벨이란 한 트랜잭션과 다른 트랜잭션이 논리적으로 어느정도의 간섭을 허용하는지를 정의한 격리의 수준을 의미한다.

Read Uncommitted, Read Committed(Oracle default), Repeatable Read(MySQL Default), Serializable로 나뉜다.

Read Uncommitted

가장 약한 수준의 격리.

한 트랜잭션이 select를 수행할 때, 다른 트랜잭션이 커밋을 수행하지 않아도 변경된 상태로 보이게 되는 수준이다. 이럴경우 Dirty Read가 발생한다.

Dirty Read

한 트랜잭션이 Select를 수행할 때, 다른 트랜잭션에서 commit 되지 않은 갱신이 읽히는 현상.
예를들어, A 트랜잭션에서 과일 테이블을 조회할 때, B 트랜잭션이 사과 행의 개수 컬럼을 10000으로 변경했다. A는 이 결과를 읽었으나 B 트랜잭션에서 장애가 발생하여 rollback을 수행. 결과 A가 다시 읽었을 때 10000이 아닌 원래 값으로 복귀되어 읽히게 된다.

만약, 10000으로 수량을 확인하고 9000개를 주문하는 경우가 발생했다면 위 상황은 시스템 장애로 판단될 것이다.

Read Committed(Oracle Default)

Read Committed는 다른 트랜잭션에서 Commit한 결과만을 반영해 보여주는 수준의 격리이다. 이 경우 Unrepeatable read 현상이 발생하게 된다. 오라클에서는 Read Committed가 기본 격리 수준이다.

Unrepeatable Read

Select를 수행했을 때, 여러 번의 시도에서 같은 결과가 출력되지 않는 현상이다. 예를들어 A 트랜잭션이 예금 테이블을 조회했을 때, B가 이를 갱신하여 자신의 잔액을 1억으로 만들다. 아직 커밋 전이므로 A는 1억이 아닌 기존의 금액으로 읽는다. 이후 B가 커밋을 수행하면, A는 다시 동일한 Select를 실행했을 때에 1억이라는 수로 읽히게 된다.

Repeatable Read(MySQL-InnoDB Default)

이 격리 수준에서는 다른 트랜잭션의 갱신 결과가 Select에 반영되지 않는다. MySQL의 경우 스토리지 엔진이 InnoDB라면 트랜잭션이 종료될 때까지 일관된 select 결과가 나오는 것을 보장한다.(MVCC)
그러나 이 격리 수준에서도 Phantom read 현상이 발생할 수 있다.

Phantom read

Select결과 이전에는 없던 데이터가 추가되는 현상. MySQL에서는 Next-key lock을 통해 이 현상을 해결함.

Serializable

가장 높은 수준의 격리로 실제로는 동시에 실행되더라도 모든 트랜잭션이 순차적으로 실행되는 것처럼 수행하는 격리 수준이다.

TABLE PRODUCT

IDCOST
1250
2350

직렬화 이상 (Serializable Anomaly

직렬화 이상은 성공적으로 커밋된 트랜잭션들의 결과가 해당 트랜잭션을의 가능한 어떤 순서로도 구성될 수 없을 때를 의미한다.

Transaction 1

-- BEGIN TRANSACTION
SELECT SUM(COST) FROM PRODUCT;
INSERT INTO PRODUCT VALUES (3, 300);
COMMIT;

Transaction 2

-- BEGIN TRANSACTION
SELECT SUM(COST) FROM PRODUCT;
INSERT INTO PRODUCT VALUES(4, 300);
COMMIT;

두 트랜잭션이 동시에 수행될 때, 1, 2 모두 300이라는 결과를 조회하게 된다. 그 후 각각 300을 삽입하게 된다.

양 트랜잭션이 직렬로 수행된다고 했을 때, 두 트랜잭션이 모두 300의 값을 조회하는 것은 불가능하다. 왜냐하면, 한 트랜잭션이 직렬적으로 먼저 수행된다면 필연적으로 다음 트랜잭션은 600을 조회해야만 하기 때문이다.
따라서 이런 상황이 직렬화 이상의 예시로 볼 수 있다.

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

PostgreSQL 공식 문서에 따르면, Serializable 격리 수준에서는
두 개 이상의 트랜잭션이 동시에 커밋되었을 때, 그 결과가 어떤 직렬 실행 순서로도 설명될 수 없는 경우를 허용하지 않는다.

이러한 상황이 발생하지 않도록 하기 위한 방법은 하나의 트랜잭션이 종료될 때까지는 다른 트랜잭션이 읽지 못하도록 하는 것이다.

이런 격리 수준은 쉽게 교착상태를 만들 수 있다.

(교착상태 방지에 대해서는 다음에 조사해봐야 겠다.)

MVCC(Multiversion Concurrency Control)

MySQL(InnoDB)

InnoDB는 데이터에 대한 갱신 작업에 대해서 Consistent read를 보장하기 위해 undo tablespace 라는 rollback 세그먼트를 운영한다.
어떤 데이터가 삽입 또는 갱신, 삭제 되면 이 행에 대한 로그를 rollback 세그먼트에 기록한다.
InnoDB는 모든 테이블에 대해서 3가지 추가적인 필드를 추가하여 운영한다. 1. DB_ROW_ID(6바이트), 2. DB_ROLL_PTR(7바이트), 3. DB_TRX_ID(6바이트)
Delete 연산은 내부적으로 일종의 Update 연산으로 처리한다. Update 로그를 생성하고 삭제된 데이터에 Special bit set을 수행해 삭제되었음을 기록한다.

Undo logs는 rollback 세그먼트에 저장되며 2가지로 구분된다.
1. Insert log
삽입 로그는 데이터 삽입시 생성되며, 트랜잭션이 커밋되는 즉시 제거된다. Consistent read를 위해서 사용된다.
2. Update log
갱신 로그는 Update 연산 또는 Delete 연산 시 생성되고 Consistent Read를 위해 사용된다. 다만, 트랜잭션에 커밋시 바로 삭제되지 않는다. Update log는 InnoDB가 Consistent read를 위해 할당한 스냅샷을 가진 트랜잭션이 존재하지 않을 때 제거된다. 왜냐하면 Consistent Read를 할 때, 기존 값을 복원하기 위해 해당 로그를 사용하기 때문이다.

로그를 제거하는 과정은 데이터에 대한 갱신, 삭제 연산과 동일한 시간을 소모하며 꽤 빠르게 수행된다.

커밋을 자주 하는 것과 Consistent read를 하는 트랜잭션을 많이 사용하는게 권장되기는 하지만, InnoDB가 갱신로그를 버리지 못한다면 rollback segment가 너무 비대해지게 된다. 이 Rollback segment는 undo tablespace라는 공간을 가지고 있는데 바로 이 공간이 실제로 로그들이 저장되는 곳이다.

특히나 삭제에 대한 갱신로그는 Purge 라고 부르는 작업으로 제거된다.(Purge 명령어 호출 가능) Purge를 수행하는 스레드가 별도로 존재하기 때문이다.
물론 로그의 사이즈가 실제 데이터보다는 작지만, 적은 단위의 데이터를 지속적으로 쓰고, 지우는 과정을 계속 반복하게 되면 Purge 스레드가 점점 데이터 갱신 속도에 뒤쳐지게 되고, 연산의 수행 속도를 제한하게 되면서 동시에 innodb_max_puge_lag 환경 변수를 수정함으로써 더 많은 자원을 소비하게 된다.

https://dev.mysql.com/doc/refman/8.4/en/innodb-multi-versioning.html

profile
Samsung Electronics.

0개의 댓글